Шаблон Variadic компилируется только при объявлении пересылки

У меня есть шаблон variadic, который наследует от всех аргументов шаблона:

template <typename... Ts>
struct derived : Ts...
{
};

Я также хотел бы иметь возможность для выражения типа "существующий derived с добавленными аргументами шаблона". Моя попытка это:

// Do not ODR-use (goes in namespace impl or similar)!
template<class ... NewInputs, class ... ExistingInputs>
auto addedHelper(const derived<ExistingInputs...>&)
    -> derived<ExistingInputs..., NewInputs...>;

template<class ExistingInput, class ... NewInputs>
using Added = decltype(addedHelper<NewInputs...>(std::declval<ExistingInput>()));

В качестве простого примера Added<derived<A, B>, C> должны быть derived<A, B, C>. Я использую вспомогательную функцию для вывода аргументов шаблона из первого пакета параметров.

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

Почему этот код не компилируется :

#include <utility>

template <typename... Ts>
struct derived : Ts...
{};

template<class ... NewInputs, class ... ExistingInputs>
auto addedHelper(const derived<ExistingInputs...>&)
    -> derived<ExistingInputs..., NewInputs...>;

template<class ExistingInput, class ... NewInputs>
using Added = decltype(addedHelper<NewInputs...>(std::declval<ExistingInput>()));


struct A;
struct B;
struct C;

// Goal: This forward declaration should work (with incomplete A, B, C).
auto test(derived<A, B> in) -> Added<decltype(in), C>;


struct A {};
struct B {};
struct C {};

void foo()
{
    auto abc = test({});

    static_assert(std::is_same_v<decltype(abc), derived<A, B, C>>, "Pass");
}

Принимая во внимание, что этот код компилируется :

#include <utility>

template <typename... Ts>
struct derived;


template<class ... NewInputs, class ... ExistingInputs>
auto addedHelper(const derived<ExistingInputs...>&)
    -> derived<ExistingInputs..., NewInputs...>;

template<class ExistingInput, class ... NewInputs>
using Added = decltype(addedHelper<NewInputs...>(std::declval<ExistingInput>()));


struct A;
struct B;
struct C;

// Goal: This forward declaration should work (with incomplete A, B, C).
auto test(derived<A, B> in) -> Added<decltype(in), C>;


template <typename... Ts>
struct derived : Ts...
{};


struct A {};
struct B {};
struct C {};

void foo()
{
    auto abc = test({});

    static_assert(std::is_same_v<decltype(abc), derived<A, B, C>>, "Pass");
}

Для удобства, здесь оба случая одновременно (комментарий in/out #define FORWARD_DECLARED): https://godbolt.org/z/7gM52j

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

Ответ 1

Наблюдение за Evg бьет ногтем по голове: проблема здесь в ADL. Это на самом деле та же проблема, с которой я столкнулся с этим вопросом.

Проблема заключается в следующем: у нас есть неквалифицированный звонок здесь:

template<class ExistingInput, class ... NewInputs>
using Added = decltype(addedHelper<NewInputs...>(std::declval<ExistingInput>()));
//                     ^^^^^^^^^^^

Мы знаем, что шаблон функции, потому что мы находим его при помощи регулярного поиска, поэтому мы не имеем дела с целым "является < оператором или шаблон интродьюсера" вопросом. Однако, поскольку это неквалифицированный вызов, мы также должны выполнить аргумент-зависимый поиск.

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

struct X {
    friend void foo(X) { }
};
foo(X{}); // must work, call the hidden friend defined within X

В результате в нашем звонке идет речь:

auto test(derived<A, B> in) -> Added<decltype(in), C>;

Мы должны создать экземпляр derived<A, B>... но этот тип наследует от двух неполных классов, что мы не можем сделать. Там, где проблема, там, где мы терпим неудачу.

Вот почему версия для предварительной декларации работает. template <typename... T> struct derived; является неполным, поэтому просто попытаться заглянуть внутрь него для поиска функций-друзей, тривиально ничего не находит - нам не нужно создавать что-либо еще.

Аналогично, версия, где derived была полной, но на самом деле ничего не происходила, также будет работать.


К счастью, это можно исправить в связи с тем, что предложил Evg. Сделайте квалифицированный звонок:

template<class ExistingInput, class ... NewInputs>
using Added = decltype(::addedHelper<NewInputs...>(std::declval<ExistingInput>()));

Это позволяет избежать ADL, который вы даже не хотели. В лучшем случае вы избегаете делать то, что не приносит вам никакой пользы. Плохой случай, ваш код не компилируется. Злой случай, для некоторых входов вы случайно вызываете другую функцию полностью.


Или просто используйте Boost.Mp11 mp_push_back