功能模板声明顺序影响可见性(有时)

时间:2009-04-29 20:53:11

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

我正在尝试创建一个函数:

template <typename T>
void doIt( T*& p )
{
   if ( !p ) { return; }
   T& ref = *p;
   getClassName( ref );
}

其中行为根据传入的p的类型而有所不同。特别是,调用的getClassName版本应取决于p的类型。在以下示例中,我可以成功调用:

doIt<myClass1>( myClass1*& )
doIt<myClass1<int> >( myClass1*& )
doIt<myClass2>( myClass2*& )
doIt<myClass2<int> >( myClass2*& )

但是当我打电话时它失败了:

doIt< std::vector<int, std::allocator<int> > >( std::vector<int, std::allocator<int>>*& )

错误:

a.cxx: In function ‘void doIt(T*&) [with T = std::vector<int, std::allocator<int> >]’:
ba.cxx:87:   instantiated from here
a.cxx:33: error: invalid initialization of reference of type ‘MyClass1&’ from expression of type ‘std::vector<int, std::allocator<int> >’
a.cxx:16: error: in passing argument 1 of ‘const char* getClassName(MyClass1&)’

(gcc 4.2.4)。

如果我移动声明:

template<typename T, typename A>
char const* getClassName( std::vector<T,A>& ) { printf("std::vector<T,A>\n"); return NULL; }
之前 - 然后它编译。所以,

  • 为什么getClassName( std::vector<T,A>& )出现在doIt之前,而不是getClassName( MyClass2T<T>& )
  • 如何使doIt独立于std::vector? (我希望能够将doIt放在自己的标题中,而不必了解std::vector或任何特定用户(将由用户定义)。

#include <stdio.h>
#include <assert.h>
#include <vector>

//template<typename T>
//char const* getClassName( T& );

//template<typename T, typename A>
////char const* getClassName( std::vector<T,A>& ) { printf("std::vector<T,A>\n"); return NULL; }

#if 1
// ---------  MyClass2
struct MyClass1
{};

char const* getClassName( MyClass1& ) { printf("MyClass1\n"); return NULL; }

// ---------  MyClass1T
template< typename T>
struct MyClass1T
{};

template<typename T>
char const* getClassName( MyClass1T<T>& ) { printf("MyClass1T<T>\n"); return NULL; }
#endif


template <typename T>
void doIt( T*& p )
{
   if ( !p ) { return; }
   T& ref = *p;
   getClassName( ref );
}


// ---------  MyClass2
struct MyClass2
{};


// declared after doIt, OK.
char const* getClassName( MyClass2& ) { printf("MyClass2\n"); return NULL; }

// ---------  MyClass2T
template< typename T>
struct MyClass2T
{};

// declared after doIt, OK.
template<typename T>
char const* getClassName( MyClass2T<T>& ) { printf("MyClass2T<T>\n"); return NULL; }

template<typename T, typename A>
char const* getClassName( std::vector<T,A>& ) { printf("std::vector<T,A>\n"); return NULL; }



void test()
{
#if 1
   MyClass1 mc1;
   MyClass1* mc1p = &mc1;
   doIt( mc1p );

   MyClass2 mc2;
   MyClass2* mc2p = &mc2;
   doIt( mc2p );

   MyClass1T<int> mc1t;
   MyClass1T<int>* mc1tp = &mc1t;
   doIt( mc1tp );

   MyClass2T<int> mc2t;
   MyClass2T<int>* mc2tp = &mc2t;
   doIt( mc2tp );

   // Nested templates are OK.
   MyClass2T<MyClass1> mc2t2;
   MyClass2T<MyClass1>* mc2tp2 = &mc2t2;
   doIt( mc2tp2 );
#endif

#if 1
   std::vector<int> v;
   std::vector<int>* vp = &v;
   doIt( vp );                   // FAIL!
#endif
}

2 个答案:

答案 0 :(得分:4)

  

为什么getClassName(std :: vector&amp;)必须出现在doIt之前,而不是getClassName(MyClass2T&amp;)

任何功能都需要范围内的声明。当您使用vector<int>实例化模板函数时,它需要一个带有签名getClassName(vector<int>&)的函数(至少是一个原型),以便编译成功。

  

我该怎么办才能使它独立于std :: vector? (我希望能够将doIt放在自己的头文件中,而不必了解std :: vector或任何特殊化,这将是用户定义的)

阅读FAQ on Templates。在第一次实例化doIt之前,尝试放置所有doIt依赖模板函数的原型。

答案 1 :(得分:4)

失败的原因是在实例化时,不会发生函数的非限定名称查找(但只有ADL - Argument Dependent Lookup)。实例化上下文(取自C ++标准的14.6.4.1/6):

  

依赖于模板参数的表达式的实例化上下文是在同一翻译单元中模板专门化实例化之前声明的外部链接的声明集。

在这种情况下,您调用的所有模板特化的实例化点都在test(读14.6.4.1/1)的定义之后。因此,您声明的所有函数都在test函数中使用非限定查找可见,但查找它们实际上对于函数调用是不同的:

依赖于模板中模板参数的函数调用将按以下方式查找:

  • 普通查找和ADL都会考虑模板定义上下文中的名称。
  • 实例化上下文中的名称仅被视为ADL。

这意味着因为在模板的定义上下文中没有声明合适的getClassName函数,所以必须使用ADL在实例化上下文中找到合适的函数 - 否则调用将失败并且找不到任何声明。

参数依赖查找(ADL)

对于类型为std::vector<T>的参数,ADL会搜索名称空间std中的函数和T的名称空间。将getClassName函数放入std命名空间对此有用(但标准不允许这样做,因为这会导致未定义的行为 - 这应该只作为最后的手段来完成)。

要查看ADL的效果,请尝试使用doIt而不是MyClass2的向量调用int。从那时起T = MyClass2,ADL将在MyClass2的命名空间中搜索接受std::vector<MyClass2>的合适函数并将成功 - 与使用int时相反,只会查看进入std

对于其他函数调用,它们各自的声明也都被发现,因为它们都在全局命名空间中声明,其中函数调用的参数类型也被定义(MyClass1,{{1}等等。)

C ++常见问题解答很好,但它没有深入到模板中(没有发现任何提及ADL的内容)。有一个专门的template faq处理一些更复杂的陷阱。


小心未定义的行为

请注意,即使您在 MyClass2函数(而不是之前)之后显示的声明,许多编译器也会接受代码。但正如上面的标准引用所述,那么声明将不是实例化上下文的一部分,并且test中的规则将被观察:

  

如果调用结果不正确或者找到更好的匹配,则相关命名空间内的查找会考虑所有翻译单元中这些命名空间中引入外部链接的所有函数声明,而不仅仅是考虑模板中找到的那些声明定义和模板实例化上下文,然后程序有未定义的行为。

因此,似乎工作的将是未定义的行为。编译器接受它是有效的,但它同样适用于拒绝它或崩溃和终止的编译器。因此,请注意所需的任何名称在实例化上下文中确实可见。

希望这有帮助。