Насколько полезно наследовать конструкторов на С++?

Поскольку я сижу на заседаниях Комитета по стандартам С++, они обсуждают плюсы и минусы отбрасывания 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

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