使用new(this)重用构造函数

发布于 2024-08-25 17:40:53 字数 907 浏览 3 评论 0原文

这是最近在我担任助教的班级中出现的。我们正在教学生如何在 c++ 中进行复制构造函数,而最初教 java 的学生问是否可以从一个构造函数调用另一个构造函数。我知道答案是否定的,因为他们在课堂上的代码中使用了迂腐的标志,而旧标准不支持这一点。我在 Stackoverflow 和其他网站上发现了一个建议,使用 new (this) 来伪造它,如下所示。

class MyClass
{
    private:
        int * storedValue;
    public:
        MyClass(int initialValue = 0)
        {
            storedValue = new int(initialValue);
        }

        ~ MyClass()
        {
            delete storedValue;
        }

        MyClass(const MyClass &b)
        {
            new (this) MyClass(*(b.storedValue));
        }

        int value() {
            return *storedValue;
        }
};

这是非常简单的代码,显然不会通过重用构造函数来保存任何代码,但它只是为了例子。

我的问题是,这是否符合标准,以及是否应该考虑任何边缘情况,从而阻止其成为健全的代码?

编辑:我应该指出,这对我来说似乎非常危险,但这更多是从我不真正了解它而不是知道它如何变坏的角度来看。我只是想确保如果学生问起这个问题,我可以指导他们为什么可以或不应该这样做。我已经建议他们出于所有实际目的使用共享初始化方法。这更多的是一个教学问题,而不是一个实际项目。

This came up recently in a class for which I am a teaching assistant. We were teaching the students how to do copy constructors in c++, and the students who were originally taught java asked if you can call one constructor from another. I know the answer to this is no, as they are using the pedantic flag for their code in class, and the old standards do not have support for this. I found on Stackoverflow and other sites a suggestion to fake this using new (this) such as follows

class MyClass
{
    private:
        int * storedValue;
    public:
        MyClass(int initialValue = 0)
        {
            storedValue = new int(initialValue);
        }

        ~ MyClass()
        {
            delete storedValue;
        }

        MyClass(const MyClass &b)
        {
            new (this) MyClass(*(b.storedValue));
        }

        int value() {
            return *storedValue;
        }
};

This is really simple code, and obviously does not save any code by reusing the constructor, but it is just for example.

My question is if this is even standard compliant, and if there are any edge cases that should be considered that would prevent this from being sound code?

Edit: I should note that this seems very dangerous to me, but that is more from a view point of I don't really understand it more than knowing how it can go bad. I just wanted to make sure that if asked about it by students that I can direct them towards why the can or shouldn't do it. I already suggested to them for all practical purposes to use a shared initialization method. This is more of a teaching question than for a practical project.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(7

乱世争霸 2024-09-01 17:40:53

C++0x 将引入允许构造函数调用其他构造函数的语法。

在此之前,new(this)某些情况下有效,但并非全部。特别是,一旦进入构造函数,您的基类就已经完全构造完毕。通过 new(this) 进行重构会重新调用基类构造函数,而不调用基类析构函数,因此如果基类没有预料到这种黑客行为,那么就会出现问题——而且它们可能没有。

一个清晰的例子:

class Base
{
public:
   char *ptr;
   MyFile file;
   std::vector vect;
   Base()
   {
       ptr = new char[1000];
       file.open("some_file");
   }
   ~Base()
   {
       delete [] ptr;
       file.close();
   }
};

class Derived : Base
{
    Derived(Foo foo)
    {
    }
    Derived(Bar bar)
    {
       printf(ptr...);  // ptr in base is already valid
       new (this) Derived(bar.foo); // ptr re-allocated, original not deleted
       //Base.file opened twice, not closed
       // vect is who-knows-what
       // etc
    }
}

或者正如他们所说的“欢闹随之而来”

C++0x will introduce syntax to allow constructors to call other constructors.

Until then, new(this) works in some cases, but not all. In particular, once in the constructor, your base class(es) are already fully constructed. Reconstructing via new(this) re-calls the base constructors without calling the base destructors, so expect problems if the base classes weren't expecting this kind of hackery - and they probably weren't.

An example for clarity:

class Base
{
public:
   char *ptr;
   MyFile file;
   std::vector vect;
   Base()
   {
       ptr = new char[1000];
       file.open("some_file");
   }
   ~Base()
   {
       delete [] ptr;
       file.close();
   }
};

class Derived : Base
{
    Derived(Foo foo)
    {
    }
    Derived(Bar bar)
    {
       printf(ptr...);  // ptr in base is already valid
       new (this) Derived(bar.foo); // ptr re-allocated, original not deleted
       //Base.file opened twice, not closed
       // vect is who-knows-what
       // etc
    }
}

or as they say 'hilarity ensues'

隔岸观火 2024-09-01 17:40:53

成员和基类将在进入构造函数主体之前初始化,然后在调用第二个构造函数时再次初始化。一般来说,这会导致内存泄漏和可能的未定义行为。

所以答案是“不,这不是健全的代码”。

The members and base classes will be initialised before entering the constructor body, then initialised again when you call the second constructor. In general this will lead to memory leaks and possibly undefined behaviour.

So the answer is "no, this is not sound code".

浮世清欢 2024-09-01 17:40:53

以下是 C++ FAQ 在问题中对此的说法,“一个类的一个构造函数可以调用同一个类的另一个构造函数来初始化 this 对象吗?”:

顺便说一句,不要尝试通过放置新的方式来实现这一点。有些人认为他们可以在 Foo::Foo(char) 正文中说出 new(this) Foo(x, int(x)+7)。然而这很糟糕,很糟糕,很糟糕。请不要写信给我并告诉我它似乎可以在您的特定编译器的特定版本上运行;这很糟糕。构造函数在幕后做了很多神奇的小事情,但这种糟糕的技术会影响那些部分构造的部分。就说不吧。

Here is what C++ FAQ has to say about it, in the question, “Can one constructor of a class call another constructor of the same class to initialize the this object?”:

BTW do NOT try to achieve this via placement new. Some people think they can say new(this) Foo(x, int(x)+7) within the body of Foo::Foo(char). However that is bad, bad, bad. Please don't write me and tell me that it seems to work on your particular version of your particular compiler; it's bad. Constructors do a bunch of little magical things behind the scenes, but that bad technique steps on those partially constructed bits. Just say no.

雨落□心尘 2024-09-01 17:40:53

除非您尝试调用父级的构造函数,否则我建议您创建一个私有初始化方法。没有理由不能在构造函数中调用共享初始值设定项。

Unless you're trying to call a parent's constructor, I'd suggest making a private initialization method. There's no reason you couldn't call a shared initializer across your constructors.

心奴独伤 2024-09-01 17:40:53

如果您有这样的构造函数,则这不起作用:

class MyClass {
public:
    MyClass( const std::string & PathToFile )
    : m_File( PathToFile.c_str( ) )
    {
    }
private:
    std::ifstream m_File;
}

无法恢复原始参数,因此您无法从复制构造函数调用该构造函数。

This does not work if you have a constructor like that:

class MyClass {
public:
    MyClass( const std::string & PathToFile )
    : m_File( PathToFile.c_str( ) )
    {
    }
private:
    std::ifstream m_File;
}

The original argument cannot be recovered, so you cannot call that constructor from a copy-constructor.

倦话 2024-09-01 17:40:53

当确切的代码被编写出来时,它应该可以工作——尽管我无法想象为什么你要编写这样的代码。特别是,它取决于以下事实:所有指针仅用于引用单个 int。既然如此,为什么他们不直接将 int 放入对象中,而是使用指针并动态分配 int 呢?简而言之,它们所拥有的内容很长且效率低下,但与以下内容没有显着不同:

class MyClass {
    int v;
public:
    MyClass(int init) : v(init) {}
    int value() { return v; }
};

不幸的是,当您尝试从指针中获得一些实际用途(例如,在不同对象中分配不同数量的内存)时,它们使用的“技巧” placement new 停止工作——这完全取决于每个对象分配完全相同的内存量的事实。由于每个分配都仅限于完全相同的分配,为什么要将该分配放入堆中而不是使其成为对象本身的一部分?

说实话,在某些情况下这样做是有意义的。然而,我立即想到的唯一一个问题是分配很大,并且您运行在堆空间比堆栈空间多得多的环境中。

该代码可以工作,但仅在相当狭窄的特定情况下才有用。我并不认为我会推荐它作为如何做事的例子。

As that exact code is written, it should work -- though I can't imagine exactly why you'd write code like that. In particular, it depends on the fact that all the pointer is only ever used to refer to a single int. That being the case, why didn't they just put an int in the object, instead of using a pointer and allocating the int dynamically? In short, what they have is long and inefficient, but not significantly different from:

class MyClass {
    int v;
public:
    MyClass(int init) : v(init) {}
    int value() { return v; }
};

Unfortunately, the minute you try to get some real use from the pointer (e.g., allocating different amounts of memory in different objects) the "trick" they using with placement new quits working -- it depends completely on the fact that every object is allocating exactly the same amount of memory. Since you're limited to exactly the same allocation in each, why put that allocation in the heap instead of making it part of the object itself?

Truthfully, there are circumstances when it would make sense. The only one I can think of right off, however, is that the allocation is large and you're running in an environment where there's a lot more heap space than stack space.

The code works, but it's only useful under fairly narrow, specific circumstances. It doesn't strike me as something I'd recommend as an example of how to do things.

瀞厅☆埖开 2024-09-01 17:40:53

在我看来,如果您知道自己在做什么,即使在派生类的构造函数中也可以安全地使用 new(this) 。您只需确保您的基类有一个虚拟构造函数(其基类也具有相同的构造函数,一直到链的下游)。例如:

#include <stdio.h>
#include <new>

struct Dummy {};

struct print
{
    print(const char *message)                    { fputs(message, stdout); }
    print(const char *format, int arg1)           { printf(format, arg1); }
    print(const char *format, int arg1, int arg2) { printf(format, arg1, arg2); }
};
struct print2 : public print
{
    print2(const char *message)                    : print(message) {}
    print2(const char *format, int arg1)           : print(format, arg1) {}
    print2(const char *format, int arg1, int arg2) : print(format, arg1, arg2) {}
};

class foo : public print
{
    int *n;
public:
    foo(Dummy) : print("foo::foo(Dummy) {}\n") {}
    foo() : print("foo::foo() : n(new int) {}\n"), n(new int) {}
    foo(int n) : print("foo::foo(int n=%d) : n(new int(n)) {}\n", n), n(new int(n)) {}
    int Get() const { return *n; }
    ~foo()
    {
        printf("foo::~foo() { delete n; }\n");
        delete n;
    }
};

class bar : public print2, public foo
{
public:
    bar(int x, int y) : print2("bar::bar(int x=%d, int y=%d) : foo(x*y) {}\n", x, y), foo(x*y) {}
    bar(int n) : print2("bar::bar(int n=%d) : foo(Dummy()) { new(this) bar(n, n); }\n", n), foo(Dummy())
    {
        __assume(this); // without this, MSVC++ compiles two extra instructions checking if this==NULL and skipping the constructor call if it does
        new(this) bar(n, n);
    }
    ~bar()
    {
        printf("bar::~bar() {}\n");
    }
};

void main()
{
    printf("bar z(4);\n");
    bar z(4);
    printf("z.Get() == %d\n", z.Get());
}

输出:

bar z(4);
bar::bar(int n=4) : foo(Dummy()) { new(this) bar(n, n); }
foo::foo(Dummy) {}
bar::bar(int x=4, int y=4) : foo(x*y) {}
foo::foo(int n=16) : n(new int(n)) {}
z.Get() == 16
bar::~bar() {}
foo::~foo() { delete n; }

如果基类具有常量*或引用成员(或者如果您无法编辑包含基类声明的文件),那么您当然不走运。这将使得在其中编写虚拟构造函数变得不可能 - 更不用说使用“new(this)”,然后您将初始化这些“常量”成员两次!这就是真正的东西,C++0x 委托构造函数,可以真正派上用场的地方。

请告诉我该技术是否还有其他不安全或不可移植的地方。

(编辑:我还意识到,也许在虚拟类中,虚拟函数表可能会被初始化两次。这将是无害的,但效率低下。我需要尝试一下,看看编译后的代码是什么样子。)

*如果你仅在基类中具有常量成员(并且没有引用),您并非完全不走运。您只需确保所有常量成员的所有类都有自己的虚拟构造函数,基类的虚拟构造函数可以依次调用它们。不过,如果某些常量具有像 int 这样的内置类型,那么你就不走运了——这些常量将不可避免地被初始化(例如,const int 将被初始化为零)。

编辑:这是一个链接虚拟构造函数的示例,如果 int value 在类 FooBar 内变为 const int value,该构造函数将会被破坏:

#include <stdio.h>
#include <new>

struct Dummy {};

struct print
{
    print(const char *message)                    { fputs(message, stdout); }
    print(const char *format, int arg1)           { printf(format, arg1); }
    print(const char *format, int arg1, int arg2) { printf(format, arg1, arg2); }
};
struct print2 : public print
{
    print2(const char *message)                    : print(message) {}
    print2(const char *format, int arg1)           : print(format, arg1) {}
    print2(const char *format, int arg1, int arg2) : print(format, arg1, arg2) {}
};

class FooBar : public print
{
    int value;
public:
    FooBar() : print("FooBar::FooBar() : value(0x12345678) {}\n"), value(0x12345678) {}
    FooBar(Dummy) : print("FooBar::FooBar(Dummy) {}\n") {}
    int Get() const { return value; }
};

class foo : public print
{
    const FooBar j;
    int *n;
public:
    foo(Dummy) : print("foo::foo(Dummy) : j(Dummy) {}\n"), j(Dummy()) {}
    foo() : print("foo::foo() : n(new int), j() {}\n"), n(new int), j() {}
    foo(int n) : print("foo::foo(int n=%d) : n(new int(n)), j() {}\n", n), n(new int(n)), j() {}
    int Get() const { return *n; }
    int GetJ() const { return j.Get(); }
    ~foo()
    {
        printf("foo::~foo() { delete n; }\n");
        delete n;
    }
};

class bar : public print2, public foo
{
public:
    bar(int x, int y) : print2("bar::bar(int x=%d, int y=%d) : foo(x*y) {}\n", x, y), foo(x*y) {}
    bar(int n) : print2("bar::bar(int n=%d) : foo(Dummy()) { new(this) bar(n, n); }\n", n), foo(Dummy())
    {
        printf("GetJ() == 0x%X\n", GetJ());
        __assume(this); // without this, MSVC++ compiles two extra instructions checking if this==NULL and skipping the constructor call if it does
        new(this) bar(n, n);
    }
    ~bar()
    {
        printf("bar::~bar() {}\n");
    }
};

void main()
{
    printf("bar z(4);\n");
    bar z(4);
    printf("z.Get() == %d\n", z.Get());
    printf("z.GetJ() == 0x%X\n", z.GetJ());
}

输出:

bar z(4);
bar::bar(int n=4) : foo(Dummy()) { new(this) bar(n, n); }
foo::foo(Dummy) : j(Dummy) {}
FooBar::FooBar(Dummy) {}
GetJ() == 0xCCCCCCCC
bar::bar(int x=4, int y=4) : foo(x*y) {}
foo::foo(int n=16) : n(new int(n)), j() {}
FooBar::FooBar() : value(0x12345678) {}
z.Get() == 16
z.GetJ() == 0x12345678
bar::~bar() {}
foo::~foo() { delete n; }

(0xCCCCCCCC 是未初始化的内存被初始化的内容在调试版本中。)

It seems to me that it's possible to use new(this) safely even in the constructor of a derived class, if you know what you're doing. You just have to make sure that your base class has a dummy constructor (and the same for its base class, all the way down the chain). For example:

#include <stdio.h>
#include <new>

struct Dummy {};

struct print
{
    print(const char *message)                    { fputs(message, stdout); }
    print(const char *format, int arg1)           { printf(format, arg1); }
    print(const char *format, int arg1, int arg2) { printf(format, arg1, arg2); }
};
struct print2 : public print
{
    print2(const char *message)                    : print(message) {}
    print2(const char *format, int arg1)           : print(format, arg1) {}
    print2(const char *format, int arg1, int arg2) : print(format, arg1, arg2) {}
};

class foo : public print
{
    int *n;
public:
    foo(Dummy) : print("foo::foo(Dummy) {}\n") {}
    foo() : print("foo::foo() : n(new int) {}\n"), n(new int) {}
    foo(int n) : print("foo::foo(int n=%d) : n(new int(n)) {}\n", n), n(new int(n)) {}
    int Get() const { return *n; }
    ~foo()
    {
        printf("foo::~foo() { delete n; }\n");
        delete n;
    }
};

class bar : public print2, public foo
{
public:
    bar(int x, int y) : print2("bar::bar(int x=%d, int y=%d) : foo(x*y) {}\n", x, y), foo(x*y) {}
    bar(int n) : print2("bar::bar(int n=%d) : foo(Dummy()) { new(this) bar(n, n); }\n", n), foo(Dummy())
    {
        __assume(this); // without this, MSVC++ compiles two extra instructions checking if this==NULL and skipping the constructor call if it does
        new(this) bar(n, n);
    }
    ~bar()
    {
        printf("bar::~bar() {}\n");
    }
};

void main()
{
    printf("bar z(4);\n");
    bar z(4);
    printf("z.Get() == %d\n", z.Get());
}

Output:

bar z(4);
bar::bar(int n=4) : foo(Dummy()) { new(this) bar(n, n); }
foo::foo(Dummy) {}
bar::bar(int x=4, int y=4) : foo(x*y) {}
foo::foo(int n=16) : n(new int(n)) {}
z.Get() == 16
bar::~bar() {}
foo::~foo() { delete n; }

Of course you're out of luck if the base class has constant* or reference members (or if you can't edit the file containing the base class's declaration). That would make it impossible to write a dummy constructor in it — not to mention that with "new(this)", you'd then be initializing these "constant" members twice! That's where the real thing, C++0x delegating constructors, could really come in handy.

Please tell me if there's anything else about this technique that can still be unsafe or non-portable.

(Edit: I've also realized that maybe in a virtual class, the virtual function table might be initialized twice. That would be harmless, but inefficient. I need to try that and see what the compiled code looks like.)

*If you merely have constant members (and no references) in the base class, you're not completely out of luck. You can just make sure all the classes of all the constant members have their own dummy constructors that the base class's dummy constructor can call in turn. You're out of luck if some of the constants have built-in types like int, though — those will be initialized unavoidably (e.g., a const int will be initialized to zero).

Edit: Here's an example of chaining dummy constructors, that would be broken if int value became const int value inside class FooBar:

#include <stdio.h>
#include <new>

struct Dummy {};

struct print
{
    print(const char *message)                    { fputs(message, stdout); }
    print(const char *format, int arg1)           { printf(format, arg1); }
    print(const char *format, int arg1, int arg2) { printf(format, arg1, arg2); }
};
struct print2 : public print
{
    print2(const char *message)                    : print(message) {}
    print2(const char *format, int arg1)           : print(format, arg1) {}
    print2(const char *format, int arg1, int arg2) : print(format, arg1, arg2) {}
};

class FooBar : public print
{
    int value;
public:
    FooBar() : print("FooBar::FooBar() : value(0x12345678) {}\n"), value(0x12345678) {}
    FooBar(Dummy) : print("FooBar::FooBar(Dummy) {}\n") {}
    int Get() const { return value; }
};

class foo : public print
{
    const FooBar j;
    int *n;
public:
    foo(Dummy) : print("foo::foo(Dummy) : j(Dummy) {}\n"), j(Dummy()) {}
    foo() : print("foo::foo() : n(new int), j() {}\n"), n(new int), j() {}
    foo(int n) : print("foo::foo(int n=%d) : n(new int(n)), j() {}\n", n), n(new int(n)), j() {}
    int Get() const { return *n; }
    int GetJ() const { return j.Get(); }
    ~foo()
    {
        printf("foo::~foo() { delete n; }\n");
        delete n;
    }
};

class bar : public print2, public foo
{
public:
    bar(int x, int y) : print2("bar::bar(int x=%d, int y=%d) : foo(x*y) {}\n", x, y), foo(x*y) {}
    bar(int n) : print2("bar::bar(int n=%d) : foo(Dummy()) { new(this) bar(n, n); }\n", n), foo(Dummy())
    {
        printf("GetJ() == 0x%X\n", GetJ());
        __assume(this); // without this, MSVC++ compiles two extra instructions checking if this==NULL and skipping the constructor call if it does
        new(this) bar(n, n);
    }
    ~bar()
    {
        printf("bar::~bar() {}\n");
    }
};

void main()
{
    printf("bar z(4);\n");
    bar z(4);
    printf("z.Get() == %d\n", z.Get());
    printf("z.GetJ() == 0x%X\n", z.GetJ());
}

Output:

bar z(4);
bar::bar(int n=4) : foo(Dummy()) { new(this) bar(n, n); }
foo::foo(Dummy) : j(Dummy) {}
FooBar::FooBar(Dummy) {}
GetJ() == 0xCCCCCCCC
bar::bar(int x=4, int y=4) : foo(x*y) {}
foo::foo(int n=16) : n(new int(n)), j() {}
FooBar::FooBar() : value(0x12345678) {}
z.Get() == 16
z.GetJ() == 0x12345678
bar::~bar() {}
foo::~foo() { delete n; }

(The 0xCCCCCCCC is what uninitialized memory is initalized with in the Debug build.)

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文