【问题标题】:Namespace causes sub-optimal template overload resolution命名空间导致次优模板重载解决方案
【发布时间】:2015-08-26 20:07:24
【问题描述】:

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

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

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

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,标准说:

在解析从属名称时,会考虑来自以下来源的名称:

  • 在模板定义处可见的声明。

  • 来自与函数参数类型相关的命名空间的声明,来自实例化上下文和定义上下文。

这对我来说有点奇怪;我不明白为什么第一个要点会指定声明必须在模板的 definition 处可见,而不是在 instantiation 处可见,因为第二个要点明确指定一些声明仅在实例化点可见是允许的。 (如果有人想提供一个理由,我会很感激,但这不是我的问题,因为我的理解是“标准委员会为什么决定 X”形式的问题是题外话。)

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

但标准中“与函数参数类型相关的命名空间”究竟是什么意思? using ::MyClassThatCanDoStuff 声明不应该与命名空间内scoped_foo 实例类型的显式作用域一起触发依赖于参数的查找,并且该查找不应该找到doStuff() 的非断言定义吗?

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

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

【问题讨论】:

    标签: c++ templates namespaces argument-dependent-lookup


    【解决方案1】:

    有了命名空间,doStuff 当然是一个从属名。

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

    当函数调用 (5.2.2) 中的 后缀表达式unqualified-id,平时不考虑的其他命名空间 可以搜索不合格的查找(3.4.1),并且在这些命名空间中, 命名空间范围的友元函数或函数模板声明 (11.3) 可能会发现其他不可见的内容。

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

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

    在一个表达式中:

    后缀表达式(表达式列表opt

    如果 postfix-expression 是一个 unqualified-id,则 unqualified-id 表示 从属名称 if [...]。如果运算符的操作数是依赖于类型的表达式,则该运算符还表示 从属名称。此类名称不受约束,可查阅 模板实例化的点(14.6.4.1)在两个上下文中 模板定义和实例化点的上下文

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

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

    模板定义中使用的非依赖名称使用 通常的名称查找并在它们使用时绑定。

    找不到您以后的重载声明。


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


    但是标准中的“与 函数参数的类型”?using ::MyClassThatCanDoStuff 声明不应该一起 具有范围内的 scoped_foo 实例类型的显式范围 命名空间,触发依赖参数的查找

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

    对于函数调用中的每个参数类型T,都有一组 零个或多个关联命名空间和一组零个或多个 要考虑的相关类。命名空间集和 类完全由函数参数的类型决定 (以及任何模板模板参数的命名空间)。 用于指定类型的 Typedef 名称和 using-declaration 不构成此集合。 命名空间和类的集合是 通过以下方式确定:

    • 如果 T 是基本类型,[...]

    • 如果 T 是类类型(包括联合),则其关联的类是:类本身;它所属的类别(如有的话);及其 直接和间接基类。其关联的命名空间是 其关联类的最内层封闭命名空间。此外, 如果 T 是一个类模板特化,它的关联命名空间和 类还包括:与 为模板类型参数提供的模板参数的类型 (不包括模板模板参数);其中任何的命名空间 模板模板参数是成员;以及其中任何类别 用作模板模板参数的成员模板是成员。 [ 注意:非类型模板参数对关联的命名空间集没有贡献。 —尾注 ]

    • [...]

    【讨论】:

      【解决方案2】:

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

      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
      }
      

      【讨论】:

      • ....呵呵。是的,这似乎可行(尽管在涉及多个文件的非平凡案例中必须声明模板专业化inline)。知道为什么重载版本不起作用而专业化版本起作用吗?
      【解决方案3】:

      来自与函数参数类型相关的命名空间的声明,来自实例化上下文和定义上下文。

      以下代码示例,打印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&amp;) 时,第二个要点会将B::foo 添加到重载列表中。

      为什么模板专业化在这种情况下有效

      只有一个功能:

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

      即使稍后定义。 请注意,翻译单元中应该知道专业化这一事实,否则程序格式错误(不尊重 ODR)。

      而重载则不会。

      你认为:

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

      没错。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2020-11-24
        • 1970-01-01
        • 2012-10-25
        • 2010-09-05
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多