Подтвердить что ты не робот

Использование нового (этого) для повторного использования конструкторов

Это появилось недавно в классе, для которого я преподаватель. Мы преподавали студентам, как делать копии конструкторов в С++, а учащиеся, которые изначально преподавали 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;
        }
};

Это действительно простой код и, очевидно, не сохраняет какой-либо код, повторно используя конструктор, но это, например, просто.

Мой вопрос в том, является ли это стандартным, и если есть какие-либо случаи ребер, которые следует учитывать, которые бы помешали этому быть звуковым кодом?

Изменить. Я должен отметить, что это кажется мне очень опасным, но это больше похоже на точку зрения. Я действительно не понимаю его больше, чем зная, как это может ухудшиться. Я просто хотел убедиться, что если спросить об этом ученики, я могу направить их к тому, почему это может или не должно делать. Я уже предложил им для всех практических целей использовать общий метод инициализации. Это скорее вопрос преподавания, чем практический проект.

4b9b3361

Ответ 1

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

или, как говорится, "веселье"

Ответ 2

Члены и базовые классы будут инициализированы перед вводом в тело конструктора, а затем снова инициализируются при вызове второго конструктора. В общем случае это приведет к утечке памяти и, возможно, поведению undefined.

Итак, ответ "нет, это не звуковой код".

Ответ 3

Вот что должно знать об этом в C + + FAQ в вопросе : "Может ли один конструктор класса вызвать другой конструктор тот же класс для инициализации этого объекта?":

Кстати, не пытайтесь достичь этого путем размещения нового. Некоторые люди думают, что они могут сказать new(this) Foo(x, int(x)+7) в теле Foo::Foo(char). Однако это плохо, плохо, плохо. Пожалуйста, не пишите мне и не говорите мне, что она работает над вашей конкретной версией вашего конкретного компилятора; это плохо. Конструкторы делают кучу маленьких магических вещей за кулисами, но эта неудачная техника работает на этих частично сконструированных битах. Просто скажите "нет".

Ответ 4

Если вы не пытаетесь вызвать родительский конструктор, я бы предложил сделать частный метод инициализации. Нет причин, по которым вы не могли бы вызвать общий инициализатор в своих конструкторах.

Ответ 5

Это не работает, если у вас есть такой конструктор:

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

Исходный аргумент не может быть восстановлен, поэтому вы не можете вызвать этот конструктор из конструктора-копии.

Ответ 6

Поскольку этот точный код написан, он должен работать, хотя я не могу точно представить, почему вы должны писать такой код. В частности, это зависит от того, что все указатели используются только для обозначения одного int. В таком случае, почему они просто не помещали int в объект, вместо того, чтобы использовать указатель и динамически выделять int? Короче говоря, то, что у них длительное и неэффективное, но не сильно отличается от:

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

К сожалению, в ту минуту, когда вы пытаетесь получить реальное использование от указателя (например, выделяя разное количество памяти в разных объектах), "трюк", который они используют при работе с новыми рабочими местами размещения, полностью зависит от того, что каждый объект выделяет точно такой же объем памяти. Поскольку вы ограничены одним и тем же распределением в каждом, зачем ставить это распределение в кучу, а не делать его частью самого объекта?

Истинно, есть обстоятельства, когда это имеет смысл. Единственный, о котором я могу думать сразу, заключается в том, что выделение является большим, и вы работаете в среде, где есть намного больше места для кучи, чем пространство стека.

Код работает, но он полезен только при довольно узких особых обстоятельствах. Это не кажется мне чем-то, что я бы рекомендовал в качестве примера того, как делать вещи.

Ответ 7

Мне кажется, что можно использовать новое (это) безопасно даже в конструкторе производного класса, если вы знаете, что делаете. Вам просто нужно убедиться, что ваш базовый класс имеет фиктивный конструктор (и тот же для его базового класса, вплоть до цепочки). Например:

#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)" вы бы дважды инициализировали эти "постоянные" члены! То, что реальная вещь, конструкторы делегирования С++ 0x, действительно может пригодиться.

Скажите, пожалуйста, что-нибудь еще об этом методе, который все еще может быть небезопасным или не переносимым.

(Edit: Я также понял, что, возможно, в виртуальном классе таблица виртуальных функций может быть инициализирована дважды. Это было бы безобидно, но неэффективно. Мне нужно попробовать это и посмотреть, как выглядит скомпилированный код.)

* Если у вас просто есть постоянные члены (и нет ссылок) в базовом классе, вам не совсем не повезло. Вы можете просто убедиться, что все классы всех константных членов имеют свои собственные конструкторы-заглушки, которые конструктор-конструктор базового класса может вызвать по очереди. Вам не повезло, если у некоторых констант есть встроенные типы, такие как int, но они неизбежно инициализируются (например, const int будет инициализироваться ноль).

Изменить: Здесь пример цепочки конструкторов-заглушек, который был бы сломан, если значение int стало const int value внутри класса 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());
}

Вывод:

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 - это то, что неинициализированная память инициализируется в сборке Debug.)