接口检测






4.95/5 (32投票s)
2007年7月3日
14分钟阅读

83886

362
检测类中成员是否存在。
目录
引言
在任何C++程序中,调用一个未声明的函数都会导致编译错误。检测类的接口可以允许程序员在不产生此类错误的情况下检查给定公共成员函数或数据是否存在,从而在成员不存在时指定不同的行为。接口检测不使用任何继承属性,因此提供了更清晰、更安全的新解决方案。本文通过探讨和解释一些高级C++主题,描述了一种实现此类检测功能的方法。
激励性示例
假设我们要编写一个容器类,它提供所有标准的容器操作,如push
、pop
、insert
等,并添加了其他任何容器中未见的全新功能。我们将把这个新的容器类称为MetaContainer
。它的实现依赖于一个给定的经典容器——通常是STL容器,如std::vector
和std::list
——但也接受用户定义的容器。因此,MetaContainer
类定义了一个接受基本容器类的模板参数。
用法
MetaContainer< int, std::vector > myContainer1;
MetaContainer< double, std::list > myContainer2;
MetaContainer< int, UserContainer > myContainer3;
程序员可以选择底层容器来调整性能。例如,如果对集合的中间部分进行大量插入操作,那么std::list
比std::vector
是更好的选择。然而,Container
模板参数不必遵循严格的接口。例如,std::list
提供一个remove
函数,而std::vector
则没有。MetaContainer
类确实提供了一个remove
函数,其实现调用底层容器的remove
函数以及其他内部操作。
void MetaContainer::Remove(...)
{
...
m_UnderlyingContainer.remove(...);
...
}
如果底层容器类,如std::vector
或其他用户定义的容器,没有声明remove
函数,那么将使用一个通用的、性能较低的remove
算法。要应用此策略,我们需要回答以下问题:我们如何知道容器类是否有一个remove
函数,以便在容器不提供该函数时,我们可以静默地切换到通用的remove
实现?
所需技术背景
在解释解决方案之前,必须回顾一下在接口检测实现中使用的一些关键C++概念。在本章中,以下主题将以非常简洁的方式进行讨论:
- 成员指针的语法和行为
- 依赖名称
- 成员指针作为非类型模板参数
- SFINAE
如果您已经了解了所有这些概念,可以直接跳到下一章。
成员指针
引言
成员指针可分为4类
- 非静态成员函数的指针
- 非静态数据成员的指针
- 静态成员函数的指针
- 静态数据成员的指针
示例
struct MyClass
{
void MF (int); // non static Member Function
int MD; // non static Data Member
static void Static_MF (int); // static Member Function
static int Static_MD; // static Data Member
}
在本章的其余部分,我们将参考这4个成员声明。
成员指针的语法
上述每个成员的指针都采用同一种语法
成员 | 指针 |
MF | &MyClass::MF |
MD | &MyClass::MD |
Static_MF | &MyClass::Static_MF |
Static_MD | &MyClass::Static_MD |
由于成员指针的语法包含类名,因此此类指针可以成为依赖名称。相反,普通函数或数据的指针则不能。依赖名称是依赖于模板参数的名称。例如:
template< class T >
void f ()
{
...
pf = &T::MF; // pointer dependent on the T parameter
}
名称依赖属性在我们的解决方案中起着重要作用。
成员指针的类型
成员函数指针的类型
成员函数指针具有以下类型:
指针 | 类型 |
&MyClass::MF |
void (MyClass::*)(int) |
&MyClass::Static_MF |
void (*)(int) |
请注意,静态成员函数的指针类型可能具有误导性。非静态成员函数遵循特定的约定:它添加了一个隐式参数,该参数接受一个对象指针。静态成员函数不适用于对象,也没有隐式的对象参数。此类函数的指针与普通函数指针具有相同的类型。 1 重要的是,非静态和静态成员函数指针的类型是不同的,并且彼此不兼容。
数据成员指针的类型
静态和非静态数据成员指针的类型与它们的函数对应物一样,遵循相同的语法差异。
指针 | 类型 |
&MyClass::MD |
int MyClass::* |
&MyClass::Static_MD |
int* |
静态和非静态成员指针的类型再次不同且不兼容。这是因为后者与对象相关,因此包含偏移量而不是地址。
成员指针作为非类型模板参数
成员指针可以是 것입니다非类型模板参数。语法很简单。例如,我们将使用上面看到的每种类型的指针作为模板参数:
template < void (MyClass::*)(int) >
void f1 () {}
template < void (*)(int) >
void f2 () {}
template < int MyClass::* >
void f3 () {}
template < int* >
void f4 () {}
f1 < &MyClass::MF > (); // ok
f1 < &MyClass::Static_MF > (); // Error: type mismatch
f2 < &MyClass::Static_MF > (); // ok
作为模板参数的成员指针必须是模板声明中指定的精确类型。不可能进行转换。
SFINAE
SFINAE,"**S**ubstitution **F**ailure **I**s **N**ot **A**n **E**rror" 的缩写,是在函数重载解析过程中起作用的一个原则:如果模板函数的实例化产生无效的参数或返回类型,编译器会默默地将不合法的函数实例化从重载解析集中移除。例如:
struct Test
{
typedef int Type;
};
template < typename T >
void f( typename T::Type ) {} // definition #1
template < typename T >
void f( T ) {} // definition #2
f< Test > ( 10 ); //call #1
f< int > ( 10 ); //call #2 without error thanks to SFINAE
如果没有SFINAE,第二次调用将在#1中替换模板参数时产生错误。从第二次调用实例化#1的结果是:void f( typename int::Type ) {}
。多亏了SFINAE,结果函数实例化不会产生错误,因为有另一个函数匹配调用。
接口检测实现
成员函数检测
如引言所述,使用未声明函数的标识符会产生编译错误。检查函数是否存在,逻辑上需要使用其标识符,因为我们不使用任何继承属性或任何额外信息。我们必须找到一种机制,在标识符指向未声明的函数时不会产生错误。SFINAE非常适合这项任务。
由于成员函数指针可以是依赖名称,因此我们可以对它们使用SFINAE原则。然而,SFINAE应用于函数的参数类型或返回类型。成员函数指针不是类型,不能直接用于函数签名。为了克服这一点,函数指针被用作模板参数:
template < void (MyClass::*)() >
struct TestNonStatic { };
使用TestNonStatic
结构,我们可以在函数参数或返回类型中使用非静态成员函数的指针。
template < class T >
TestNonStatic<&T::foo> Test( );
只要重载解析集中存在另一个函数,SFINAE就不会产生错误。我们需要声明一个函数,当成员函数(示例中的foo
)不存在时,该函数将作为“后备”使用。具有省略号参数的函数是完成此工作的合适候选,因为这样的函数在重载解析过程中优先级最低,并且匹配任何参数。
template < class T >
void Test( ... );
具有省略号参数的函数是否总是重载解析集中的优先级最低的函数?C++标准是这么说的,Visual C++ 8是这么说的,GCC则不然(在我们的例子中)。 2替代解决方案是简单地使用TestNonStatic
结构作为Test
函数的参数而不是返回类型。
下一步是找到一种在编译时知道哪个Test
函数将被选择的方法。模板元编程中常用的技巧是为每个函数声明指定不同的返回类型,并使用sizeof
对函数调用进行操作。返回类型必须具有不同的尺寸才能从sizeof
获得不同的结果。sizeof
操作数不会被评估。这就是为什么sizeof
内调用的函数不需要定义。下面的示例确定类MyClass
是否包含成员函数void foo()
。
// Return types for sizeof
typedef char NotFound;
struct NonStaticFound { char x[2]; };
struct StaticFound { char x[3]; };
// Test Structures for SFINAE
template < void (MyClass::*)() >
struct TestNonStatic ;
template < void (*)() >
struct TestStatic ;
// Overload functions
template < class T >
StaticFound
Test( TestStatic< &T::foo >* );
template < class T >
NonStaticFound
Test( TestNonStatic< &T::foo >* );
template < class T >
NotFound
Test( ... );
check_presence = sizeof( Test< MyClass >( 0 ) );
最后一步是将整个机制封装到一个可重用类中,您可以从中指定要测试的类和函数签名。然而,函数的标识符(在上面的示例中为foo
)不能作为可重用类的参数指定。当前接口检测实现中提供了预处理器宏,以满足此需求。这些宏的使用在最后一章中有详细介绍。
最后一个考虑:常量成员函数呢?上面的代码无法检测到它们。如果用户为可重用检测器类指定了常量成员签名,它将无法编译,因为我们使用给定的函数签名来构造TestNonStatic
结构的模板参数,我们也将其用于TestStatic
结构。静态成员函数不能是常量的;没有隐式对象来应用常量限定符。使用两个接口,一个用于检测成员函数,一个用于检测常量成员函数,对用户来说会太麻烦。
在检测器类中添加一个具有常量成员签名的测试结构似乎可以解决这个问题。
template < void (MyClass::*)() const >
struct TestNonStaticConst ;
将函数Test( TestNonStatic< &T::foo >* )
和Test( TestNonStaticConst< &T::foo >* )
放在同一个重载集中会导致歧义解析错误,当给定类同时具有完全相同的签名时,它既有一个常量成员函数,又有一个非常量成员函数。我们需要使用省略号作为第二个参数来降低这两个函数之一的优先级。
// Test Structures for SFINAE
template < void (MyClass::*)() >
struct TestNonStatic ;
template < void (*)() >
struct TestStatic ;
template < void (T::*)() const >
struct TestNonStaticConst ;
// Overloaded functions
template < class U >
NonStaticFound
Test( TestNonStatic< &U::aff >*, ... );
template < class U >
NonStaticFound
Test( TestNonStaticConst< &U::aff >*, int );
template < class U>
StaticFound
Test( TestStatic< &U::aff >*, int );
template < class U >
NotFound
Test( ... );
check_presence = sizeof( Test( 0,0 ) );
数据成员检测
在了解了如何实现成员函数检测之后,对数据成员进行同样的检测是很直接的。事实上,这更容易,因为我们不必担心常量成员问题。唯一的更改涉及TestStatic
和TestNonStatic
结构的模板参数。
// Return types for sizeof
typedef char NotFound;
struct NonStaticFound { char x[2]; };
struct StaticFound { char x[3]; };
// Test Structures for SFINAE
template < int MyClass::* > // change 1 of 2
struct TestNonStatic ;
template < int * > // change 2 of 2
struct TestStatic ;
// Overload functions
template < class T >
StaticFound
Test( TestStatic< &T::foo >* );
template < class T >
NonStaticFound
Test( TestNonStatic< &T::foo >* );
template < class T >
NotFound
Test( ... );
已知限制和问题
当前接口检测器的限制分为两类:
- 编译问题:任何导致编译错误的情况。
- 设计限制:使用当前接口检测实现和设计警告不可能做到的事情。
编译问题
有两种编译错误:
- 直接来自C++标准的错误
- 编译器特定的错误
标准编译错误
访问检查错误
接口检测器仅关注公共成员。然而,如果传递给检测器的函数碰巧存在于类的私有或保护部分,编译器将发出“访问被拒绝”错误。因为类成员访问检查发生在名称查找和重载解析之后,SFINAE不会消除错误。
编译器特定的错误
本节列出了最新C++编译器生成的非标准错误。当然,早期对模板支持不完整的编译器,如Visual C++ 6,可能会产生一些错误,但这里未列出。下表展示了当前测试编译器的检测功能和错误。
简单的成员函数 | 重载的成员函数 | 成员函数模板特化 | 数据成员 | |
Visual C++ 8 | ![]() |
![]() |
![]() |
![]() |
Visual C++ 7.1 | ![]() |
![]() |
![]() |
![]() |
GCC 4.1 | ![]() |
![]() |
![]() |
![]() |
Comeau | ![]() |
![]() |
![]() |
![]() |
Visual C++数据成员检测错误
在Visual C++中,由静态或非静态数据成员指针组成的依赖名称在替换过程中会产生错误,如果数据成员不是内置类型。
示例
struct Y {};
struct X
{
Y a;
};
template < Y X::* >
struct Test ;
template < class T >
void f (Test< &T::a >*) {}
f< X >(0); // error on Visual c++ 8
上面的代码根据C++标准是良构的。GCC和Comeau编译它,但Visual C++不编译。这对我们的接口检测器产生了有害影响,因为数据成员检测包含在SFINAE机制中,因此不会产生错误。这导致了错误的行为,返回NOT_FOUND
值,尽管数据成员确实存在。因此,数据成员检测宏对Visual C++是禁用的。
设计限制
精确签名
接口检测器检查精确签名,不进行任何转换。例如,如果您想检查void foo(int)
是否存在,您将无法检测到一个兼容的函数,如void foo(double)
。此限制的一个重要副作用是无法检测继承的函数。这是因为此类函数的隐式参数的类型是指向声明函数的父类的指针。
语义差异
第二个限制是检测到的函数与实际使用函数之间可能存在语义差异。例如,在生物学中,有些细胞可以克隆。我可以通过检测对象是否包含clone
函数来检查这种“可克隆”能力。然而,一些甚至不是细胞的类可能声明了一个具有其他用途的clone
函数,即虚构造函数。
用法
提供的接口检测器的使用依赖于4个宏:
CREATE_FUNCTION_DETECTOR
CREATE_DATA_DETECTOR
DETECT_FUNCTION
DETECT_DATA
前两个宏用于从函数或数据标识符构建检测器。这是在进行实际检测之前的第一步。例如,如果我想检测函数int foo (double)
,我首先需要构建检测器:
CREATE_FUNCTION_DETECTOR(foo);
请注意,一旦为foo
构建了检测器,就可以检测到与foo
标识符关联的任何签名:int foo(double)
、void foo()
等。DETECT_FUNCTION
和DETECT_DATA
宏执行检测。这两个宏的第一个参数是受检测的类的名称。其余参数遵循函数或数据的声明语法,除了需要用逗号分隔标识符,以将它们与类型的其余部分分开。
// detection: int foo (double, int)
DETECT_FUNCTION ( MyClass, int, foo, (double, int) )
// detection: const int bar
DETECT_DATA ( MyClass, const int, bar )
// detection: void foo()
DETECT FUNCTION ( MyClass, void, foo, () )
DETECT
宏返回以下不言自明的常量:
NOT_FOUND
(== 0)STATIC_FUNCTION
NON_STATIC_FUNCTION
STATIC_DATA
NON_STATIC_DATA
所有这些常量都属于Detector
命名空间。作为一个简单的例子,假设我们要检查一个类是否包含成员函数void Print()
。如果函数可用,我们就调用它。否则,我们打印一条“没有可用的Print函数”消息。我们将测试以下2个结构:
struct X
{
void Print()
{
std::cout << "X Print" << std::endl ;
}
};
struct Y
{};
首先,我们需要构建检测器:
#include "Detector.h"
CREATE_FUNCTION_DETECTOR(Print);
其次,我们必须定义一个结构,该结构将用于根据函数是否存在来选择不同的行为。该结构的第一个模板参数将用于保存DETECT_FUNCTION
宏的结果。第二个参数是要测试的类。
template < int, class T >
struct Select
{
static void Print ( T obj )
{
obj.Print();
}
};
template < class T >
struct Select < Detector::NOT_FOUND , T >
{
static void Print ( ... )
{
std::cout << "No Print function" << std::endl;
}
};
// Helper function
template < class T >
void PrintHelper( T a )
{
Select< DETECT_FUNCTION ( T, void, Print, () ) , T >::Print( a );
}
现在我们可以安全地在任何类型为X
或Y
的对象上调用PrintHelper
。
X a;
Y b;
PrintHelper(a); // "X Print"
PrintHelper(b); // "No Print function"
选择正确行为的过程在编译时完成。可以一次检查整个类接口。例如,假设任何可以飞行和嘎嘎叫的物体都是鸭子。为了根据此定义知道一个类是否代表一只鸭子,我们可以在单个表达式中检查Fly
和Quack
函数是否都存在。
DETECT_FUNCTION( Class, void, Fly, () ) &
DETECT_FUNCTION( Class, void, Quack, () )
一种更好的方法是定义一个类似这样的宏来执行多个函数或数据检查:
#define DUCK_INTERFACE( Class ) \
DETECT_FUNCTION( Class, void, Fly, () ) & \
DETECT_FUNCTION( Class, void, Quack, () )
这样,可以使用简单易懂的表达式来检测一个类是否是鸭子,并且可以重复使用。DUCK_INTERFACE( MyDuckClass )
在MyDuckClass
不严格遵循鸭子接口时返回Detector::NOT_FOUND
。
现在,这里是“激励性示例”的解决方案,即“我们如何知道容器类是否有一个remove
函数?”
CREATE_FUNCTION_DETECTOR(remove);
template < class T, template < class , class > class Container >
int HasRemove ()
{
return DETECT_FUNCTION( Container< T >, void, remove , (const T& ) );
}
HasRemove< int, std::vector >(); // NOT_FOUND
HasRemove< int, std::list >(); // NON_STATIC_FUNCTION
结论
接口检测为特定问题带来了独特的解决方案。它还可以用于支持任何鸭子类型 3设计——例如基于策略的设计——从而实现更安全、更清晰、更扩展的设计。还可以检查BCCL 4,这使得这类设计更加健壮。接口检测实现严重依赖于许多高级C++技术,特别是模板技术。因此,它有一些不可避免的缺点:代码复杂性、仅受最新编译器支持、在编译器之间穷尽且同等地跟踪错误的困难等。幸运的是,下一个C++标准 5应该会简化此类模板解决方案的编程。
注释
[1] 为了仅重载解析的目的,编译器假定静态成员函数接受一个隐式对象参数。[2] GCC认为,如果一个函数与其他函数具有相同的声明,并且在参数列表末尾添加了省略号,那么这两个函数具有相同的优先级。
[3] 在鸭子类型系统中,对象的价值决定了对象的行为。C++通过使用模板来实现静态形式的鸭子类型。 维基百科上的鸭子类型
[4] BCCL: Boost Concept Check Library
[5] Bjarne Stroustrup, A Brief Look at C++0x
参考文献
- David Vandevoorde, Nicolai M. Josuttis。C++ Templates: The Complete Guide。Addison Wesley, 2002。
- Andrei Alexandrescu。Modern C++ Design: Generic Programming and Design Patterns Applied。Addison Wesley, 2001。
历史
- 08-01-2007:
- 已编辑并移至CodeProject.com主文章库。
- 07-24-2007:
- [文章] 添加了列出编译器特定错误的表;在“编译器特定错误”中添加了Visual C++ 7.1的错误。
- [文章] 从
Test
函数的声明中删除了static
限定符,因为我们不在类定义上下文中。
- 07-07-2007:
- [文章] 目录已修复。
- [文章] 表达式“隐藏对象参数”已替换为“隐式对象参数”,这是C++标准中使用的确切措辞。
- [文章] 在“设计限制,精确签名”中添加了关于无法检测继承函数的简要说明。
- 07-03-2007:
- 初始版本