命名空间导致次优模板重载解析

Namespace causes sub-optimal template overload resolution

这与 this question 非常相似,但我不确定那里的答案是否完全适用于我放在一起演示该问题的最少代码。 (我的代码 not 使用 trailing-return 类型,并且还有一些其他差异。)此外,MSVC 的行为是否合法的问题似乎没有得到解决。

简而言之,当函数模板位于命名空间内时,我看到编译器 select 是通用函数模板实例化,而不是更具体的重载。

考虑以下一组命名空间和 class 定义:

namespace DoStuffUtilNamespace
{
  template<typename UNKNOWN>
  void doStuff(UNKNOWN& foo)
  {
    static_assert(sizeof(UNKNOWN) == -1, "CANNOT USE DEFAULT INSTANTIATION!");
  }
}

class UtilForDoingStuff
{
  public:
    template <typename UNKNOWN>
      void doStuffWithObjectRef(UNKNOWN& ref)
      {
        DoStuffUtilNamespace::doStuff(ref);
      }
};

class MyClassThatCanDoStuff { };

namespace DoStuffUtilNamespace
{
  using ::MyClassThatCanDoStuff;      // No effect.
  
  void doStuff(MyClassThatCanDoStuff& foo) { /* No assertion! */ }
}

...以及以下用例:

int main()
{
  MyClassThatCanDoStuff foo;
  DoStuffUtilNamespace::MyClassThatCanDoStuff scoped_foo;
  UtilForDoingStuff util;
  
  DoStuffUtilNamespace::doStuff(foo);         // Compiles
  DoStuffUtilNamespace::doStuff(scoped_foo);  // Compiles
  util.doStuffWithObjectRef(foo);             // Triggers static assert
  util.doStuffWithObjectRef(scoped_foo);      // Triggers static assert
}

如果删除整个 DoStuffUtilNamespace 并将其所有成员移至全局范围,则 G++ 和 Clang++ 可以正常编译。

有了命名空间,doStuff当然是从属名了。根据top-voted answer on the similar question,标准说:

In resolving dependent names, names from the following sources are considered:

  • Declarations that are visible at the point of definition of the template.

  • Declarations from namespaces associated with the types of the function arguments both from the instantiation context and from the definition context.

我觉得这有点奇怪;我不明白为什么第一个要点会指定声明必须在模板的 definition 点而不是 instantiation[= 点可见57=],因为第二个要点明确指定 一些 声明仅在实例化时可见 允许的。 (如果有人愿意提供理由,我将不胜感激,但这不是我的问题,因为我的理解是“标准委员会为什么决定 X”形式的问题不在主题范围内。)

所以我 认为 这解释了为什么 util.doStuffWithObjectRef(foo); 触发静态断言: doStuff(MyClassThatCanDoStuff&) 尚未在 [=16 的定义点声明=].事实上,在定义 doStuff 重载 class UtilForDoingStuff 之后移动 定义 似乎可以解决问题。

但是标准中“与函数参数类型关联的命名空间”到底是什么意思? using ::MyClassThatCanDoStuff 声明和命名空间中 scoped_foo 实例类型的显式范围是否应该触发依赖于参数的查找,并且此查找不应该找到非断言定义doStuff()?

此外,整个代码使用 clang++ -ftemplate-delayed-parsing 编译,没有错误,它模拟了 MSVC 的模板解析行为。这似乎更可取,至少在这种特殊情况下,因为随时向名称空间添加新声明的能力是名称空间的主要吸引力之一。但是,如上所述,根据标准,它似乎并不完全符合法律条文。这是允许的,还是不符合规定的实例?

EDIT:: 正如 KIIV 所指出的,有一个解决方法;如果使用模板特化而不是重载,代码将编译。我仍然想知道有关标准的问题的答案。

通过模板专业化,我可以让它工作:

namespace DoStuffUtilNamespace
{
  template<typename UNKNOWN>
  void doStuff(UNKNOWN& foo)
  {
    static_assert(sizeof(UNKNOWN) == -1, "CANNOT USE DEFAULT INSTANTIATION!");
  }
}

class UtilForDoingStuff
{
  public:
    template <typename UNKNOWN>
      void doStuffWithObjectRef(UNKNOWN& ref)
      {
        DoStuffUtilNamespace::doStuff(ref);
      }
};

class MyClassThatCanDoStuff { };


namespace DoStuffUtilNamespace
{
  using ::MyClassThatCanDoStuff;
  template <> void doStuff<MyClassThatCanDoStuff>(MyClassThatCanDoStuff& foo) { /* No assertion! */ }
}


int main()
{
  MyClassThatCanDoStuff foo;
  DoStuffUtilNamespace::MyClassThatCanDoStuff scoped_foo; 
  UtilForDoingStuff util;

  DoStuffUtilNamespace::doStuff(foo);         // Compiles
  DoStuffUtilNamespace::doStuff(scoped_foo);  // Compiles
  util.doStuffWithObjectRef(foo);             // Compiles
  util.doStuffWithObjectRef(scoped_foo);      // Compiles
}

Declarations from namespaces associated with the types of the function arguments both from the instantiation context and from the definition context.

使用以下代码打印 B::foo Demo

的示例
namespace A
{
    template <typename T>
    void foo(const T&) {std::cout << "A::foo" << std::endl;}

    template <typename T>
    void bar(const T& t) {
        foo(t); // thank to ADL, it will  also look at B::foo for B::S.
    }
}

namespace B
{
    struct S {};

    void foo(const S&) {std::cout << "B::foo" << std::endl;}
}

int main()
{
    B::S s;
    A::bar(s);
}

因此在调用 ?::foo(const B::S&) 时,第二个要点将 B::foo 添加到重载列表中。

why template-specialization works in this case

只有一个函数:

template<>
void DoStuffUtilNamespace::doStuff<MyClassThatCanDoStuff>(MyClassThatCanDoStuff& foo);

即使是后面定义的。 请注意,在翻译单元中应该知道有一个专门化的事实,否则程序格式不正确(不尊重 ODR)。

while overloading doesn't.

您认为:

So I think that explains why util.doStuffWithObjectRef(foo); triggers the static assertion: doStuff(MyClassThatCanDoStuff&) hasn't been declared at the point of definition of UtilForDoingStuff::doStuffWithObjectRef<UNKNOWN>(UNKNOWN&). And indeed moving the class UtilForDoingStuff definition after the doStuff overload has been defined seems to fix the issue.

完全正确。

With the namespace, doStuff is of course a dependent name.

你的出发点是错误的。像 DoStuffUtilNamespace::doStuff(ref) 这样的合格调用没有 ADL。 [basic.lookup.argdep]/p1,强调我的:

When the postfix-expression in a function call (5.2.2) is an unqualified-id, other namespaces not considered during the usual unqualified lookup (3.4.1) may be searched, and in those namespaces, namespace-scope friend function or function template declarations (11.3) not otherwise visible may be found.

DoStuffUtilNamespace::doStuffqualified-id,而不是 unqualified-id。 ADL 不适用。

因此,DoStuffUtilNamespace::doStuff 也不是从属名称。 [temp.dep]/p1:

In an expression of the form:

postfix-expression ( expression-listopt)

其中 postfix-expression 是一个 unqualified-idunqualified-id 表示 从属名称 如果 [...]。如果运算符的操作数是依赖于类型的表达式,则该运算符还表示 依赖名称。这些名称是未绑定的,可以在 模板实例化点(14.6.4.1)在这两个上下文中 实例化点的模板定义和上下文

从属名的斜体表示本段定义术语。)

相反,根据 [temp.nondep]/p1:

Non-dependent names used in a template definition are found using the usual name lookup and bound at the point they are used.

找不到你后面的重载声明。


专业化之所以有效,是因为它仍然使用相同的函数模板声明;您只是提供了与默认实现不同的实现。


But what exactly does the standard mean by "namespaces associated with the types of the function arguments"? Shouldn't the using ::MyClassThatCanDoStuff declaration, together with the explicit scoping of the scoped_foo instance type within the namespace, trigger argument-dependent lookup

没有。 using-declarations 不影响 ADL。 [basic.lookup.argdep]/p2,强调我的:

For each argument type T in the function call, there is a set of zero or more associated namespaces and a set of zero or more associated classes to be considered. The sets of namespaces and classes is determined entirely by the types of the function arguments (and the namespace of any template template argument). Typedef names and using-declarations used to specify the types do not contribute to this set. The sets of namespaces and classes are determined in the following way:

  • If T is a fundamental type, [...]

  • If T is a class type (including unions), its associated classes are: the class itself; the class of which it is a member, if any; and its direct and indirect base classes. Its associated namespaces are the innermost enclosing namespaces of its associated classes. Furthermore, if T is a class template specialization, its associated namespaces and classes also include: the namespaces and classes associated with the types of the template arguments provided for template type parameters (excluding template template parameters); the namespaces of which any template template arguments are members; and the classes of which any member templates used as template template arguments are members. [ Note: Non-type template arguments do not contribute to the set of associated namespaces. —end note ]

  • [...]