使用显式命名空间限定符时,模板实例化行为会发生变化吗?

我一直在试验一个用于可组合管道的系统,它涉及一组可能是模板化的"阶段"。每个阶段处理自己的设置、执行和清理,模板演绎用于构建流水线使用的"状态"的最小列表。这需要相当多的样板模板代码,这已经显示出一些明显不一致的行为。尽管实验成功,但由于无效的实例化,实际将其放入我们的代码库中会导致错误。

花了一些时间来找出玩具(工作的)解决方案和更丰富的版本之间的差异,但最终缩小到显式命名空间规范。

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) {}

然后我们的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;
}

现在,我不确定我是否预料到了失败。一方面,我们创建了一个test_binder<bind_stage<int>,bind_stage<bool>>,它显然将无效的实例化bind_stage<bool>作为其类型定义的一部分。它应该无法编译。

另一方面,它纯粹是作为一个名称包含在内,而不是一个定义。在这种情况下,它可能只是一个转发声明的模板,只要外部模板中没有具体引用它,我们就会期望它工作。

我没有预料到的是,根据我是否添加了(理论上多余的)全局命名空间说明符,会出现两种不同的行为。

我已经在Visual Studio、Clang和GCC中尝试过此代码。它们都有相同的行为,这让我远离这是一个编译器错误。此行为是否由C++标准中的某些内容解释?


编辑: 丹尼尔・兰格的另一个例子,对我来说意义不大:

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
}

定义Y<X<int>>时实例化了X<int>,或者没有实例化。在非指定作用域中使用函数与任何事情有什么关系?


解决方案

模板在需要时实例化。那么,为什么在像f(Y<X<int>> {});那样执行非限定调用时,编译器会实例化X<int>,而在::f(X<Y<int>>{})中对f的调用不实例化呢?

原因是依赖于名称的名称查找(ADL)(请参阅[basic.lookup.argdep]),它仅适用于非限定呼叫。

对于调用f(Y<X<int>>{}),编译器必须在X<int>的定义中查找友元函数的声明:

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涉及专门化的模板参数类型,即函数参数的类型(哎呀...)是如此不受欢迎(因为它几乎只会带来糟糕的惊喜),以至于有人提议移除它:P0934

相关文章