【问题标题】:How to implement operator delete for C++11 impostors of C "classes"?如何为 C“类”的 C++11 冒名顶替者实现运算符删除?
【发布时间】:2014-12-08 02:05:24
【问题描述】:

我正在尝试为一堆 C“类”编写 C++11 冒名顶替者(正如 @jrok 所说,因为这些类没有像包装器那样的字段),类似于:

extern "C" {
  struct cfoo;
  cfoo * cfoo_new();
  void cfoo_free(cfoo *);
  int cfoo_bar(cfoo *, int);
} // extern "C" {
class Foo final {
    Foo() = delete;                        // Prevents
    Foo(Foo &&) = delete;                  // construction
    Foo(const Foo &) = delete;             // of this
    Foo & operator=(Foo &&) = delete;      // C++
    Foo & operator=(const Foo &) = delete; // object
public: /* Methods: */
    int bar(int v) noexcept { return cfoo_bar(cPtr(), v); }
    cfoo * cPtr() noexcept { return reinterpret_cast<cfoo *>(this); }
    static Foo * create() {
       cfoo * const f = cfoo_new();
       if (!f) throw std::bad_alloc();
       return reinterpret_cast<Foo *>(f);
    }
    // No member fields! No double dereference! No extra memory!
}; // class Foo {

但是,在 C++11 代码中,我还想做类似的事情:

Foo * foo = Foo::create();
foo->bar(42);
delete foo;                                 // (1)
{
  std::unique_ptr<Foo> pFoo(Foo::create()); // no custom deleter!
  pFoo->bar(3);
} // pFoo goes out of scope                 // (2)

这样 (1) 和 (2) 只会调用 ctest_free(x-&gt;cPtr()),其中 Test * x 是传递给 delete 运算符的指针。

在 C++11 中实现这一点的正确/最安全的方法是什么?

编辑:到目前为止,感谢您的回答,但请让我们保持主题并避免对编码实践进行咆哮。请回答问题,告诉我为什么无法归档,或者根据 ISO/IEC 14482/2011 告诉我我的代码在哪里有未定义的行为。

【问题讨论】:

  • 您的班级Foo 应该有一个成员cfoo *,而不是在Foo*cfoo* 之间进行转换。
  • 这些是“冒名顶替者”,而不是“包装者”。
  • 你到底为什么要在没有任何成员字段的情况下这样做?你认为你会从中获得什么可以想象的优势?
  • @jotik:如果你想让它保持“纯 C++”并且只依赖“由 ISO/IEC 14882 保证”的东西,那么答案“这是 UB " 就这样结束了!
  • 你不能同时拥有它。如果这是一个纯 C++ 问题,那么您的 reinterpret_cast 后跟对该对象的任何使用都是完全未定义的行为。如果我们使用成员指针的情况,您的编译器实际上会将 sizeof(Foo) 设置为 sizeof(cfoo*),如果您的 Foo 是标准布局,则基本上可以保证它只是一个指向cfoo的指针。

标签: c++ c++11 interface operator-overloading porting


【解决方案1】:

这是未定义的行为,因为您正在对不是 Foocfoo)的对象调用 Foo 的非静态成员函数。相关的标准是§9.3.1/2:

如果类 X 的非静态成员函数为不属于 X 类型或派生自X,行为未定义。

空类的成员函数也不例外。


正如许多其他人已经指出的那样,做你正在尝试的最安全和正确的方法是编写一个包装器类型。例如:

class Foo {
    std::unique_ptr<cfoo, void(*)(cfoo*)> p;
public:
    Foo() : p{cfoo_new(), cfoo_free} { if (!p) throw std::bad_alloc{}; }
    int bar(int i) noexcept { return cfoo_bar(p.get(), i); }
};

将其用法与 C 接口进行比较:

// Using the C interface
{
    cfoo* foo = cfoo_new();
    if (!foo) throw std::bad_alloc{};
    cfoo_bar(foo, 42);
    cfoo_free(foo);
}

// Using a C++ wrapper
{
    Foo foo;
    foo.bar(42);
}

当使用任何优化编译器时,这将使 C 接口的开销为零。例如,对于上述两个块,GCC 输出的程序集相同

【讨论】:

  • C 接口的开销总是为零并不完全正确,正如您在我的回答中看到的那样,将包装类的引用传递给非内联函数时涉及额外的指令。因此,C 接口在一种情况下具有可追溯的速度优势,至少以丢弃 C++ 类的所有内存安全为代价。
  • 啊,好伤心!如果没有 §9.3.1/2,这将是与 C 交互的好方法。
【解决方案2】:
// No member fields! No double dereference! No extra memory!

您似乎认为在您的班级中拥有一名成员会以某种方式增加您的内存使用量。这根本不是真的。比较您的代码示例:

Foo * foo = Foo::create();
foo->bar(42);

它应该是什么样子:

Foo foo;
foo.bar();

首先我们使用堆栈内存来存储foo,这将是sizeof(Foo*),在第二个我们使用堆栈内存来存储foo,这将是sizeof(Foo)。如果Foo 包含Foo* 类型或std::unique_ptr&lt;Foo&gt; 类型的一个成员,那么Foo 会有多大?没错,就是Foo*big。

关键区别在于第二个示例是异常安全的,不会出现内存泄漏错误,清晰紧凑。

至于担心额外的取消引用,您的foo-&gt;bar() 示例中有多少取消引用?函数本身不会发生取消引用。在foo.bar() 中出现了多少,其中bar 定义为:

int bar(int v) noexcept { return cfoo_bar(p.get(), v); }

Jarod42?同样,在函数本身内不会发生取消引用,只有一次 cfoo_bar 使用该指针。

编辑:因此,通过阅读您后来在问题上留下的 cmets,您似乎实际上优化的是传递对包装对象到一个函数。是的,这确实有一些开销。如果我定义:

void foobar(cfoo * f)
{
  cfoo_bar(f, 0);
}

然后g++ -O3 生成:

foobar(cfoo*):
    xorl    %esi, %esi
    jmp cfoo_bar

鉴于:

void foobar(Foo& f)
{
  f.bar(0);
}

生成:

foobar(Foo&):
    movq    8(%rdi), %rdi
    xorl    %esi, %esi
    jmp cfoo_bar

但这只是您为 C++ 类带来的确定性破坏所付出的代价。您提出的解决方案确实会产生与 C 版本相同的程序集,但也会有相同的内存安全问题。这并不是说,当您确实需要额外的性能指令但不是免费获得时,使用 C 风格的编码永远是错误的。

注意我必须 __attribute__((noinline)) 上面的函数只是为了防止编译器内联它们,从而消除开销。

【讨论】:

  • 1337 C++ hax0r Skillz 很满意,非常感谢您的真诚关心!其次,如果Foo 包含一个指针字段,则应将其称为FooUniquePtr 或其他名称,否则会误导程序员相信发生了单个指针取消引用。第三,由于Foo 没有可访问的构造函数,我几乎可以保证sizeof(Foo) 没有合理的应用程序。最后,不管我的理智如何,如果你回答我最初的问题,说我所要求的是否无法归档,这将是礼貌的。
【解决方案3】:

以下可能会有所帮助:

class Foo {
public:
    Foo() : p(cfoo_new()) {
       if (!p) { throw std::bad_alloc(); }
    }
    ~Foo() { cfoo_free(p); }

    Foo(const Foo&) = delete;
    Foo& operator = (const Foo&) = delete;

    int bar(int v) noexcept { return cfoo_bar(p, v); }
private:
    cfoo* p;
};

或者使用 quantdev 提到的 std::unique_ptr 甚至更好

class Foo {
public:
    Foo() : p(cfoo_new(), cfoo_free) {
       if (!p) { throw std::bad_alloc(); }
    }
    int bar(int v) noexcept { return cfoo_bar(p.get(), v); }
private:
    std::unique_ptr<Foo, void(*)(Foo*)> p;
};

所以你的调用代码变成了:

{
    Foo foo;
    foo.bar(42);
} // foo goes out of scope
{
  std::unique_ptr<Foo> pFoo(new Foo()); // no custom deleter!
  pFoo->bar(3);
} // pFoo goes out of scope

【讨论】:

  • 谢谢,但不幸的是它没有帮助。如果可能的话,我明确地试图避免实例化Foo(而且这里没有人证明这是不可能的)。
  • 为什么?我们没有看到预期的收益。
  • 我试图避免双重取消引用。使用这个“冒名顶替者”接口的程序员可能会编写像void f(Foo &amp;); 这样的函数,从而导致双重取消引用。但是我需要将其压缩到最大性能。不幸的是,解释这种情况的原因并不容易在 StackOverflow 上进行,并且目前需要花费太多时间来写下来。 ;(我知道我正在寻找的东西相当精致。也许我应该在我的问题中更清楚地说明这一点。
  • @jotik:使用Foo foo;,您在使用foo.bar(42) 的通话中没有开销
  • IME“相信我,我需要这个”会导致太空火箭在飞行途中爆炸。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2022-07-21
  • 1970-01-01
  • 2012-12-03
相关资源
最近更新 更多