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)

わかりやすくするための例:

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

メンバーと基本クラスは、コンストラクター本体に入る前に初期化され、2番目のコンストラクターを呼び出すときに再び初期化されます。一般に、これはメモリリークと、場合によっては未定義の動作につながります。

したがって、答えは「いいえ、これはサウンドコードではありません」です。

于 2010-03-22T20:39:12.980 に答える
6

これについて、C ++ FAQは、「クラスの1つのコンストラクターが、このオブジェクトを初期化するために同じクラスの別のコンストラクターを呼び出すことができますか?」という質問で述べています。

ところで、新しい配置を介してこれを達成しようとしないでください。一部の人々は彼らがnew(this) Foo(x, int(x)+7)の体の中で言うことができると思いますFoo::Foo(char)。しかし、それは悪い、悪い、悪いです。私に書いて、それがあなたの特定のコンパイラのあなたの特定のバージョンで動作するようだと私に言わないでください。悪いです。コンストラクターは舞台裏でたくさんの小さな魔法のことをしますが、その悪いテクニックはそれらの部分的に構築されたビットを踏みます。いやだっていうだけだよ。

于 2010-03-22T20:47:09.250 に答える
3

親のコンストラクターを呼び出そうとしているのでない限り、プライベート初期化メソッドを作成することをお勧めします。コンストラクター間で共有イニシャライザーを呼び出せなかった理由はありません。

于 2010-03-22T20:34:47.910 に答える
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 に答える
1

その正確なコードが書かれているので、それは機能するはずです-なぜあなたがそのようなコードを書くのか正確には想像できませんが。特に、すべてのポインタが単一のintを参照するためにのみ使用されるという事実に依存します。そういうわけで、なぜ彼らはポインタを使用してintを動的に割り当てるのではなく、単にintをオブジェクトに配置しなかったのでしょうか。要するに、彼らが持っているものは長くて非効率的ですが、以下と大きく異ならない。

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

残念ながら、ポインタから実際に使用しようとすると(たとえば、さまざまなオブジェクトにさまざまな量のメモリを割り当てる)、新しい配置で使用する「トリック」は機能しなくなります。これは、すべてのオブジェクトが割り当てられているという事実に完全に依存します。まったく同じ量のメモリ。それぞれでまったく同じ割り当てに制限されているので、オブジェクト自体の一部にするのではなく、なぜその割り当てをヒープに入れるのですか?

正直なところ、それが理にかなっている状況があります。ただし、私がすぐに考えることができるのは、割り当てが大きくスタックスペースよりもはるかに多くのヒープスペースがある環境で実行しているということだけです。

コードは機能しますが、かなり狭い特定の状況でのみ役立ちます。それは私が物事を行う方法の例として私がお勧めするものとして私を襲うものではありません。

于 2010-03-22T21:26:27.420 に答える
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)」を使用すると、これらの「定数」メンバーを2回初期化することになります。そこで、本物のC++0x委任コンストラクターが本当に役に立ちます。

このテクニックについて、まだ安全でない、または移植できない可能性があるものが他にあるかどうか教えてください。

(編集:仮想クラスでは、仮想関数テーブルが2回初期化される可能性があることにも気づきました。これは無害ですが、非効率的です。それを試して、コンパイルされたコードがどのようになるかを確認する必要があります。)

*基本クラスに定数メンバーがある(参照がない)だけの場合は、完全に運が悪いわけではありません。すべての定数メンバーのすべてのクラスに、基本クラスのダミーコンストラクターが順番に呼び出すことができる独自のダミーコンストラクターがあることを確認できます。ただし、定数の一部にintのような組み込み型がある場合は、運が悪いです。これらはやむを得ず初期化されます(たとえば、const intはゼロに初期化されます)。

編集:ダミーコンストラクターをチェーンする例を次に示します。これは、intがクラスFooBar内でconstint値になると壊れます。

#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 に答える