Насколько полезно наследовать конструкторов на С++?
Поскольку я сижу на заседаниях Комитета по стандартам С++, они обсуждают плюсы и минусы отбрасывания Inheriting Constructors, поскольку поставщик компилятора не реализовал его (смысл, который пользователи не просили об этом).
Позвольте мне быстро напомнить всем, кто наследует конструкторы:
struct B
{
B(int);
};
struct D : B
{
using B::B;
};
Некоторые поставщики предлагают, чтобы с ссылками на r-значение и вариационными шаблонами (идеальными конструкторами переадресации) было бы тривиально предоставить конструктор пересылки в наследующем классе, который мог бы избежать наследования конструкторов.
Например,
struct D : B
{
template<class ... Args>
D(Args&& ... args) : B(args...) { }
};
У меня есть два вопроса:
1) Можете ли вы предоставить примеры из реального мира (не надуманные) из вашего опыта программирования, которые значительно выиграли бы от наследования конструкторов?
2) Есть ли какие-либо технические причины, о которых вы можете подумать, что бы исключить возможность "идеальных конструкторов пересылки" как адекватную альтернативу?
Спасибо!
Ответы
Ответ 1
2) Есть ли какие-либо технические причины, о которых вы можете подумать, что бы исключить возможность "идеальных конструкторов пересылки" как адекватную альтернативу?
Я показал одну проблему с этим идеальным переходом: Пересылка всех конструкторов в С++ 0x.
Кроме того, идеальный метод переадресации не может "форсировать" ясность конструкторов базового класса: либо он всегда является конструктором преобразования, либо никогда, а базовый класс всегда будет иметь прямую инициализацию (всегда используя все конструкторы, даже явные).
Другая проблема - конструкторы-инициализаторы-списки, потому что вы не можете вывести Args
в initializer_list<U>
. Вместо этого вам нужно будет переслать базу с помощью B{args...}
(обратите внимание на фигурные скобки) и инициализировать объекты D
с помощью (a, b, c)
или {1, 2, 3}
или = {1, 2, 3}
. В этом случае Args
будет типом элементов списка инициализаторов и перенаправляет их в базовый класс. После этого конструктор-инициализатор может их получить. Это, по-видимому, вызывает ненужное раздувание кода, потому что пакет аргументов шаблона потенциально содержит множество последовательностей типов для каждой различной комбинации типов и длины, и поскольку вам нужно выбрать синтаксис инициализации, это означает:
struct MyList {
// initializes by initializer list
MyList(std::initializer_list<Data> list);
// initializes with size copies of def
MyList(std::size_t size, Data def = Data());
};
MyList m{3, 1}; // data: [3, 1]
MyList m(3, 1); // data: [1, 1, 1]
// either you use { args ... } and support initializer lists or
// you use (args...) and won't
struct MyDerivedList : MyList {
template<class ... Args>
MyDerivedList(Args&& ... args) : MyList{ args... } { }
};
MyDerivedList m{3, 1}; // data: [3, 1]
MyDerivedList m(3, 1); // data: [3, 1] (!!)
Ответ 2
Недостатки пары к предлагаемому обходному пути:
- Дольше
- Он получил больше токенов
- Он использует совершенно новые сложные функции языка.
В целом, когнитивная сложность обходного пути очень плохая. Гораздо хуже, чем, например, по умолчанию специальные функции-члены, для которых был добавлен простой синтаксис.
Мотивация реального мира для наследования конструктора: сочетания AOP реализованы с использованием повторного наследования вместо множественного наследования.
Ответ 3
В дополнение к тому, что говорили другие, рассмотрите этот искусственный пример:
#include <iostream>
class MyString
{
public:
MyString( char const* ) {}
static char const* name() { return "MyString"; }
};
class MyNumber
{
public:
MyNumber( double ) {}
static char const* name() { return "MyNumber"; }
};
class MyStringX: public MyString
{
public:
//MyStringX( char const* s ): MyString( s ) {} // OK
template< class ... Args >
MyStringX( Args&& ... args ): MyString( args... ) {} // !Nope.
static char const* name() { return "MyStringX"; }
};
class MyNumberX: public MyNumber
{
public:
//MyNumberX( double v ): MyNumber( v ) {} // OK
template< class ... Args >
MyNumberX( Args&& ... args ): MyNumber( args... ) {} // !Nope.
static char const* name() { return "MyNumberX"; }
};
typedef char YesType;
struct NoType { char x[2]; };
template< int size, class A, class B >
struct Choose_{ typedef A T; };
template< class A, class B >
struct Choose_< sizeof( NoType ), A, B > { typedef B T; };
template< class Type >
class MyWrapper
{
private:
static Type const& dummy();
static YesType accept( MyStringX );
static NoType accept( MyNumberX );
public:
typedef typename
Choose_< sizeof( accept( dummy() ) ), MyStringX, MyNumberX >::T T;
};
int main()
{
using namespace std;
cout << MyWrapper< int >::T::name() << endl;
cout << MyWrapper< char const* >::T::name() << endl;
}
По крайней мере, с MinGW g++ 4.4.1 компиляция завершилась неудачей из-за пересылки конструктора С++ 0x.
Он отлично компилируется с помощью "ручной" пересылки (закомментированные конструкторы) и предположительно/возможно также с унаследованными конструкторами?
Приветствия и hth.
Ответ 4
Я вижу проблему, когда новый класс имеет переменные-члены, которые необходимо инициализировать в конструкторе. Это будет обычный случай, так как обычно производный класс добавит какое-то состояние в базовый класс.
То есть:
struct B
{
B(int);
};
struct D : B
{
D(int a, int b) : B(a), m(b) {}
int m;
};
Для тех, кто пытается его решить: как вы различаете :B(a), m(b)
и :B(b), m(a)
? Как вы обрабатываете множественное наследование? виртуальное наследование?
Если решается только самый простой случай, на практике он будет иметь весьма ограниченную полезность. Неудивительно, что поставщики компилятора еще не выполнили предложение.
Ответ 5
Философски, я против наследования конструкторов. Если вы определяете новый класс, вы определяете, как он будет создан. Если большая часть этой конструкции может иметь место в базовом классе, тогда вам вполне разумно перенаправить эту работу на конструктор базового класса в список инициализации. Но вам все еще нужно явно это делать.