Конструктор C++: идеальная переадресация и перегрузка

У меня есть два класса, A и B, и B является производным от A. A имеет несколько конструкторов (2 в приведенном ниже примере). B имеет один дополнительный элемент для инициализации (который имеет инициализатор по умолчанию).

Как добиться того, чтобы B можно было сконструировать с помощью одного из конструкторов A без необходимости вручную переписывать все перегруженные версии конструктора из A в B?

(В приведенном ниже примере мне пришлось бы предоставить четыре конструктора для B:B():A(){}, B(string s):A(s){}, B(int b):A(),p(b){}, B(string s, int b):A(s),p(b){} вместо двух, по крайней мере, если игнорировать возможность аргументов по умолчанию).

Мой подход был идеальной переадресацией, однако следующий сценарий приводит к ошибке:

#include <utility>
#include <string>

struct A {
    A(const std::string& a) : name(a) {}
    A(){}
    virtual ~A(){}

    std::string name;
};

struct B : public A {
    template<typename... Args>
    B(Args&&... args) : A(std::forward<Args>(args)...) {}

    B(const std::string& a, int b) : A(a), p(b) {}

    int p = 0;
};

int main()
{
    B b1("foo");
    B b2("foobar", 1);
}

Для b2 GCC жалуется на no matching function for call to 'A::A(const char [5], int). Очевидно, он пытается вызвать идеальный конструктор пересылки, который явно не должен работать, вместо второго конструктора B.

Почему компилятор не видит второй конструктор и вместо этого вызывает его? Существуют ли технические причины, по которым компилятор не может найти правильный конструктор B? Как я могу исправить это поведение?

Точное сообщение об ошибке:

main.cpp: In instantiation of 'B::B(Args&& ...) [with Args = {const char (&)[5], int}]':
main.cpp:26:19:   required from here
main.cpp:15:54: error: no matching function for call to 'A::A(const char [5], int)'
     B(Args&&... args) : A(std::forward<Args>(args)...) {}
                                                      ^
main.cpp:6:5: note: candidate: A::A()
     A(){}
     ^
main.cpp:6:5: note:   candidate expects 0 arguments, 2 provided
main.cpp:5:5: note: candidate: A::A(const string&)
     A(const std::string& a) : name(a) {}
     ^
main.cpp:5:5: note:   candidate expects 1 argument, 2 provided
main.cpp:4:8: note: candidate: A::A(const A&)
 struct A {
        ^
main.cpp:4:8: note:   candidate expects 1 argument, 2 provided

person mrspl    schedule 02.04.2016    source источник
comment
По какой причине вы не используете наследование конструктора?   -  person Piotr Skotnicki    schedule 02.04.2016
comment
У наследующих конструкторов @PiotrSkotnicki есть некоторые проблемы; например см. эту статью, в которой делается попытка решить различные проблемы   -  person M.M    schedule 02.04.2016


Ответы (3)


"foobar" это const char (&) [7]. Поэтому Args лучше подходит, чем const std::string&

Таким образом, эта перегрузка выбирается:

template<typename... Args>
B(Args&&... args) : A(std::forward<Args>(args)...) {}

где Args равно const char (&) [7]

поэтому становится:

B(const char (&&args_0) [7], int&& args_1)

который перенаправляется в конструктор с двумя аргументами A... которого не существует.

Желаемое поведение заключается в том, что если вы создаете B с помощью конструктора, который работает для A, то вызывается «... Args конструктор» B, в противном случае вызывается другой конструктор B, в противном случае он завершается сбоем с «не найдено подходящего конструктора для B». ".

что-то вроде этого...

#include <utility>
#include <string>

struct A {
    A(std::string a) : name(std::move(a)) {}
    A(){}
    virtual ~A(){}

    std::string name;
};

template<class...T> struct can_construct_A
{
    template<class...Args> static auto test(Args&&...args)
    -> decltype(A(std::declval<Args>()...), void(), std::true_type());

    template<class...Args> static auto test(...) -> std::false_type;

    using type = decltype(test(std::declval<T>()...));
    static constexpr bool value = decltype(test(std::declval<T>()...))::value;
};

struct B : public A {

    template<class...Args>
    B(std::true_type a_constructable, Args&&...args)
    : A(std::forward<Args>(args)...)
    {}

    template<class Arg1, class Arg2>
    B(std::false_type a_constructable, Arg1&& a1, Arg2&& a2)
    : A(std::forward<Arg1>(a1))
    , p(std::forward<Arg2>(a2))
    {
    }

    template<typename... Args>
    B(Args&&... args)
    : B(typename can_construct_A<Args&&...>::type()
        , std::forward<Args>(args)...) {}

    int p = 0;
};

int main()
{
    B b1("foo");
    B b2("foobar", 1);
}

Увидев, что A не имеет соответствующего конструктора, почему бы ему не вернуться назад и не продолжить поиск других конструкторов B, которые могут совпадать? Есть ли технические причины?

В двух словах (и говоря очень просто), когда происходит разрешение перегрузки, компилятор делает следующее:

  1. раскрыть все шаблонные перегрузки, которые могут соответствовать заданным аргументам. Добавьте их в список (с весом, указывающим уровень специализации, который был связан с попаданием в этот список).

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

  3. сортировать списки по возрастанию «работы» или веса.

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

Компилятор делает это один раз. Это не рекурсивный поиск.

Заранее приношу свои извинения пуристам среди нас, которым это детское объяснение покажется оскорбительным :-)

person Richard Hodges    schedule 02.04.2016
comment
А фикс будет? B b2(std::string("foobar"), 1);? Добавление перегрузки конструктора, которая принимает template <std::size_t size> const char (&)[size]? - person nwp; 02.04.2016
comment
что вы хотите, чтобы фактическая функциональность была? Конструктор ...Args для B не имеет для меня никакого смысла. - person Richard Hodges; 02.04.2016
comment
Я пытаюсь использовать идеальную переадресацию, поэтому мне не нужно переписывать все перегрузки конструктора A в классе B. Желаемое поведение заключается в том, что если вы создаете B с конструктором, который работает для A, то вызывается конструктор ...Args для B, в противном случае вызывается другой конструктор для B, в противном случае происходит сбой без подходящего конструктора для B. - person nwp; 02.04.2016
comment
@nwp обновлен. решение не красивое, но это начало в правильном направлении. - person Richard Hodges; 02.04.2016
comment
Вы выполнили требования, как указано, за исключением того, что теперь переписывание всех перегрузок конструктора больше не кажется такой уж плохой идеей... возможно, просто нет способа сделать его элегантным. - person nwp; 02.04.2016
comment
@nwp иногда меньше значит больше... :-) - person Richard Hodges; 02.04.2016
comment
Большое спасибо за ответ и комментарии! Я подчеркнул в исходном вопросе, какова была первоначальная цель, как пояснил @nwp. Однако, прежде чем я закрою эту тему, я все еще не понимаю: увидев, что A не имеет соответствующего конструктора, почему бы ему не вернуться и не продолжить поиск других конструкторов B, которые могут совпадать? Есть ли технические причины? - person mrspl; 02.04.2016
comment
@mrspl добавил упрощенное объяснение. - person Richard Hodges; 02.04.2016

Опция 1

Наследовать конструкторы от класса A:

struct B : A 
{
    using A::A;
//  ~~~~~~~~~^

    B(const std::string& a, int b) : A(a), p(b) {}

    int p = 0;
};

Вариант №2

Сделайте вариативный конструктор B SFINAE-способным:

#include <utility>

struct B : A
{
    template <typename... Args, typename = decltype(A(std::declval<Args>()...))>
    //                                     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^
    B(Args&&... args) : A(std::forward<Args>(args)...) {}

    B(const std::string& a, int b) : A(a), p(b) {}

    B(B& b) : B(static_cast<const B&>(b)) {}
    B(const B& b) : A(b) {}

    int p = 0;
};
person Piotr Skotnicki    schedule 02.04.2016
comment
Вместо этого я идеально перенаправлял переменную-член. Версия SFINAE работала отлично! typename = decltype(decltype(var)(std::declval<Args>()...)) - person atablash; 23.11.2019

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

Один из способов избежать этой двусмысленности состоит в том, чтобы заставить вызывающую сторону явно выбрать, нужен ли им конструктор пересылки, с помощью фиктивного параметра:

struct B : A
{
    enum dummy_t { forwarding };
    // ...

    template<typename... Args>
    B(dummy_t, Args&&... args) : A(std::forward<Args>(args)...) {}         
};

с использованием образца:

B b2("foobar", 1);
B b(B::forwarding, "foobar");

Тогда у вас даже могут быть конструкторы A и B с одинаковыми параметрами.


Альтернативным решением вашей проблемы было бы написать using A::A; в определении B. Это похоже на предоставление B набора конструкторов, соответствующих A, и они инициализируют A, вызывая соответствующий конструктор A с теми же аргументами.

Конечно, это имеет некоторые недостатки, например. вы не можете одновременно инициализировать другие элементы B. Дополнительная литература

person M.M    schedule 02.04.2016