Использование нового (этого) для повторного использования конструкторов
Это появилось недавно в классе, для которого я преподаватель. Мы преподавали студентам, как делать копии конструкторов в С++, а учащиеся, которые изначально преподавали 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;
}
};
Это действительно простой код и, очевидно, не сохраняет какой-либо код, повторно используя конструктор, но это, например, просто.
Мой вопрос в том, является ли это стандартным, и если есть какие-либо случаи ребер, которые следует учитывать, которые бы помешали этому быть звуковым кодом?
Изменить. Я должен отметить, что это кажется мне очень опасным, но это больше похоже на точку зрения. Я действительно не понимаю его больше, чем зная, как это может ухудшиться. Я просто хотел убедиться, что если спросить об этом ученики, я могу направить их к тому, почему это может или не должно делать. Я уже предложил им для всех практических целей использовать общий метод инициализации. Это скорее вопрос преподавания, чем практический проект.
Ответы
Ответ 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.)