Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Variadic template only compiles when forward declared

I have a variadic template that inherits from all template arguments:

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

I would also like to have a facility for expressing the type of "existing derived with added template arguments". My attempt at this is:

// 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>()));

As a simple example, Added<derived<A, B>, C> should be derived<A, B, C>. I use the helper function for template argument deduction of the first parameter pack.

My problem: For some reason, I can use this successfully with incomplete types if derived has been forward declared, but not if it was defined.

Why does this code not compile:

#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");
}

Whereas this code does compile:

#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");
}

For convenience, here are both cases at once (comment in/out #define FORWARD_DECLARED): https://godbolt.org/z/7gM52j

I do not understand how code could possibly become illegal by replacing a forward declaration by the respective definition (which would otherwise just come later).

like image 471
Max Langhof Avatar asked Jun 21 '19 11:06

Max Langhof


Video Answer


1 Answers

Evg's observation hits the nail on the head: the problem here is ADL. It's actually the same problem I ran into with this question.

The issue is this: we have an unqualified call here:

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

We know it's a function template because we find it in using regular lookup, so we don't have to deal with the whole "is < an operator or a template introducer" question. However, because it's an unqualified call, we also must perform argument-dependent lookup.

ADL needs to look into the associated namespaces of all the arguments, which seems fine - we don't need complete types for that. But ADL also needs to look for potential friend functions and function templates defined within the classes. After all, this needs to work:

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

As a result, in our call in question:

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

We have to instantiate derived<A, B>... but that type inherits from two incomplete classes, which we can't do. That's where the problem is, that's where we fail.

This is why the forward declaration version works. template <typename... T> struct derived; is incomplete, so just trying to look inside of it for friend functions trivially finds nothing - we don't need to instantiate anything else.

Likewise, a version where derived was complete but didn't actually derive from anything would also work.


Thankfully, this is fixable in this context with what Evg suggested. Make a qualified call:

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

This avoids ADL, which you didn't even want. Best case, you're avoiding doing something that has no benefit to you. Bad case, your code doesn't compile. Evil case, for some inputs you accidentally call a different function entirely.


Or just use Boost.Mp11's mp_push_back

like image 146
Barry Avatar answered Oct 13 '22 10:10

Barry