我正在尝试编写C ++ 11冒名顶替者(最好由@jrok称为,因为这些类没有像包装器那样的字段)用于C“类”,类似于:
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->cPtr())
,其中Test * x
是传递给delete
运算符的指针。
在C ++ 11中实现这个的最恰当/最安全的方法是什么?
编辑:到目前为止,感谢您的回答,但是请让我们保持这个主题,并避免对编码做法咆哮。请回答这个问题,告诉我为什么这是不可能的,或者根据ISO / IEC 14482/2011告诉我我的代码在哪里有未定义的行为。
答案 0 :(得分:6)
这是未定义的行为,因为您在非Foo
(a Foo
)的对象上调用cfoo
的非静态成员函数。相关标准是§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的汇编输出与上面两个块的相同。
答案 1 :(得分:4)
// 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<Foo>
类型中的一个成员,那么Foo
会有多大?没错,它会Foo*
大。
关键的区别在于第二个例子是异常安全的,安全的内存泄漏错误,清晰和紧凑。
关于额外取消引用的担心,您的foo->bar()
示例中有多少取消引用?函数本身内不会发生去引用。 foo.bar()
中将bar
定义为:
int bar(int v) noexcept { return cfoo_bar(p.get(), v); }
by Jarod42?同样,只有cfoo_bar
使用该指针时,才会在函数本身内发生去引用。
编辑:因此,通过阅读您稍后在问题上留下的评论,似乎您实际优化的内容是将参考传递给包装器对象到函数。是的,这确实有一些开销。如果我定义:
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风格的编码是错误的。
N.B。我必须__attribute__((noinline))
上述函数只是为了防止编译器内联它们,从而消除开销。
答案 2 :(得分: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;
};
或者使用quant dev
提到的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