Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Template instantiation behaviour changes when using explicit namespace qualifier?

I have been experimenting with a system for composable pipelines, which involves a set of 'stages', which may be templated. Each stage handles its own setup, execution and cleanup, and template deduction is used to build a minimal list of 'state' used by the pipeline. This requires quite a lot of boilerplate template code, which has shown up some apparently incongruous behaviour. Despite successful experiments, actually rolling it into our code-base resulted in errors due to invalid instantiations.

It took some time to track down the difference between the toy (working) solution, and the more rich version, but eventually it was narrowed down to an explicit namespace specification.

template<typename KeyType = bool>
struct bind_stage
{
    static_assert(!std::is_same<KeyType, bool>::value, "Nope, someone default instantiated me");
};

template<typename BoundStage, typename DefaultStage>
struct test_binding {};

template<template<typename...>class StageTemplate, typename S, typename T>
struct test_binding <StageTemplate<S>, StageTemplate<T>> {};

template<typename T>
auto empty_function(T b) {}

Then our main:

int main()
{
    auto binder = test_binding<bind_stage<int>, bind_stage<>>();
    //empty_function(binder); // Fails to compile
    ::empty_function(binder); // Compiles happily
    return 0;
}

Now, I'm not sure if I expect the failure, or not. On the one hand, the we create a test_binder<bind_stage<int>,bind_stage<bool>> which obviously includes the invalid instantiation bind_stage<bool> as part of its type definition. Which should fail to compile.

On the other, it's included purely as a name, not a definition. In this situation it could simply be a forward declared template and we'd expect it to work as long as nothing in the outer template actually refers to it specifically.

What I didn't expect was two different behaviours depending on whether I added a (theoretically superfluous) global namespace specifier.

I have tried this code in Visual Studio, Clang and GCC. All have the same behaviour, which makes me lean away from this being a compiler bug. Is this behaviour explained by something in the C++ standard?


EDIT: Another example from Daniel Langr which makes less sense to me:

template <typename T>
struct X {
    static_assert(sizeof(T) == 1, "Why doesn't this happen in both cases?");
};

template <typename T>
struct Y { };

template <typename T>
void f(T) { }

int main() {
    auto y = Y<X<int>>{};
    // f(y); // triggers static assertion
    ::f(y); // does not
}

Either X<int> is instantiated while defining Y<X<int>> or it is not. What does using a function in a non-specified scope have to do with anything?

like image 953
Steelbadger Avatar asked May 10 '19 11:05

Steelbadger


1 Answers

Template are instantiated when needed. So why when one performs a non qualified call as f(Y<X<int>> {}); does the compiler instantiate X<int> while it does not when the call to f is qualified as in ::f(X<Y<int>>{})?

The reason is Agument-Dependent name Lookup(ADL) (see [basic.lookup.argdep]) that only takes place for non qualified calls.

In the case of the call f(Y<X<int>>{}) the compiler must look in the definition of X<int> for a declaration of a friend function:

template <typename T>
struct X {
    //such function will participate to the overload resolution
    //to determine which function f is called in "f(Y<X<int>>{})"
    friend void f(X&){}
};

ADL involving the type of a template argument of the specialization that is the type of the function argument (ouch...) is so miss-loved (because it almost only causes bad surprises) that there is a proposal to remove it: P0934

like image 148
Oliv Avatar answered Dec 11 '22 12:12

Oliv