22

这是最近在我担任助教的一堂课上出现的。我们正在教学生如何在 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;
        }
};

这是非常简单的代码,显然没有通过重用构造函数来节省任何代码,但这只是示例。

我的问题是,这是否符合标准,是否有任何边缘情况需要考虑以防止其成为合理的代码?

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

4

7 回答 7

19

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

在那之前,new(this)某些情况下有效,但不是全部。特别是,一旦在构造函数中,您的基类就已经完全构造好了。通过new(this)重新调用基础构造函数进行重构而不调用基础析构函数,因此如果基类不期望这种hackery,那么预计会出现问题 - 他们可能不会。

一个清晰的例子:

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
    }
}

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

于 2010-03-22T20:43:44.973 回答
11

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

所以答案是“不,这不是声音代码”。

于 2010-03-22T20:39:12.980 回答
6

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

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

于 2010-03-22T20:47:09.250 回答
3

除非您尝试调用父母的构造函数,否则我建议您使用私有初始化方法。没有理由不能跨构造函数调用共享初始化程序。

于 2010-03-22T20:34:47.910 回答
1

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

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

不幸的是,当您尝试从指针中获得一些真正的用途(例如,在不同的对象中分配不同数量的内存)时,他们使用放置 new 的“技巧”就会退出工作——这完全取决于每个对象都在分配的事实完全相同的内存量。既然你被限制在每一个中完全相同的分配,为什么要把那个分配放在堆中而不是让它成为对象本身的一部分呢?

说实话,在某些情况下它是有意义的。然而,我唯一能想到的就是分配很大,而且你在一个堆空间比堆栈空间多得多的环境中运行。

该代码有效,但仅在相当狭窄的特定情况下才有用。我不觉得它是我推荐的如何做事的例子。

于 2010-03-22T21:26:27.420 回答
1

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

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

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

于 2010-03-22T20:36:43.740 回答
0

在我看来,如果您知道自己在做什么,即使在派生类的构造函数中也可以安全地使用 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值在 FooBar 类中变为const int 值,则会被破坏:

#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 是在调试版本中初始化未初始化的内存。)

于 2012-11-15T19:27:58.420 回答