“c++ 支持多种风格。”
比雅尼·斯特劳斯特鲁普,透视 ISO C++
编程是通过用计算机的一种通用语言与机器对话来向它教授一些东西的过程。你越接近机器习语,单词就越不自然。
每种语言都有自己的表达能力。对于任何给定的概念,都有一种语言,它的描述更简单、更简洁、更详细。在汇编语言中,我们必须对任何(可能是简单的)算法给出极其丰富和精确的描述,这使得回读非常困难。另一方面,C++ 的美妙之处在于,在与机器语言足够接近的同时,这种语言携带了足够多的工具来丰富自己。
C++ 允许程序员用不同的风格来表达相同的概念,好的 C++ 看起来更自然。
首先你将看到模板和样式之间的联系,然后你将深入研究 C++ 模板系统的细节。
给定这个 C++ 片段:
double x = sq(3.14);
你能猜出 sq 是什么吗?它可能是一个宏:
#define sq(x) ((x)*(x))
一个功能:
double sq(double x)
{
return x*x;
}
一个功能模板:
template <typename scalar_t>
inline scalar_t sq(const scalar_t& x)
{
return x*x;
}
一个类型(一个类的未命名实例,衰减为双精度):
class sq
{
double s_;
public:
sq(double x)
: s_(x*x)
{}
operator double() const
{ return s_; }
};
一个全局对象:
class sq_t
{
public:
typedef double value_type;
value_type operator()(double x) const
{
return x*x;
}
};
const sq_t sq = sq_t();
不管 sq(3.14)是如何实现的,大多数人只要看着它就能猜出 sq(3.14)是干什么的。然而,视觉对等 并不意味着互换。例如,如果 sq 是一个类,向函数模板传递一个正方形将会触发一个意外的参数推导:
template <typename T> void f(T x);
f(cos(3.14)); // instantiates f<double>
f(sq(3.14)); // instantiates f<sq>. counterintuitive?
此外,您会期望尽可能高效地对每种可能的数字类型进行平方,但是不同的实现在不同的情况下可能会有不同的表现:
std::vector<double> v;
std::transform(v.begin(), v.end(), v.begin(), sq);
如果您需要转换一个序列,大多数编译器将从 sq 的最后一个实现中获得性能提升(如果 sq 是一个宏,则会出现错误)。
TMP 的目的是编写代码:
- 对人类用户来说视觉上清晰,因此没有人需要看下面。
- 从编译器的角度来看,在大多数/所有情况下都是有效的。
- 自适应程序的其余部分。 1
自适应意味着“可移植”(独立于任何特定的编译器)和“不强加约束”。sq 的实现要求它的参数从某个抽象基类派生,这不能称为自适应。
C++ 模板的真正威力是风格。比较以下等价行:
double x1 = (-b + sqrt(b*b-4*a*c))/(2*a);
double x2 = (-b + sqrt(sq(b)-4*a*c))/(2*a);
所有模板参数的计算和推导都是在编译时执行的,因此不会产生运行时开销。如果函数 sq 写得正确,第 2 行至少和第 1 行一样有效,同时也更容易阅读。
使用 sq 很优雅:
- 它使代码可读或不言自明
- 它不会带来速度损失
- 它使程序对未来的优化开放
事实上,在平方的概念已经从简单的乘法中分离出来之后,你可以很容易地插入专门化:
template <typename scalar_t>
inline scalar_t sq(const scalar_t& x)
{
return x*x;
}
template <>
inline double sq(const double& x)
{
// here, use any special algorithm you have!
}
1.1.C++ 模板
经典 C++ 语言承认两种基本类型的模板— 函数模板和类模板 2 :
下面是一个功能模板 :
template <typename scalar_t>
scalar_t sq(const scalar_t& x)
{
return x*x;
}
下面是一个类模板 :
template
<
typename scalar_t, // type parameter
bool EXTRA_PRECISION = false, // bool parameter with default value
typename promotion_t = scalar_t // type parameter with default value
>
class sum
{
// ...
};
当您为其所有参数提供合适的值时,模板会在编译期间生成实体。函数模板将产生函数,而类模板将产生类。从 TMP 的角度来看,最重要的想法可以总结如下:
- 您可以利用类模板在编译时执行计算。
- 函数模板可以从参数中自动推导出它们的参数。如果调用 sq(3.14),编译器会自动算出 scalar_t 是 double,生成函数 sq < double >,并在调用处插入。
两种模板实体都开始在尖括号中声明一个参数列表 。参数可以包括类型(用关键字 typename 或 class 声明)和非类型:整数和指针。 3
请注意,当参数列表很长,或者您只想单独注释每个参数时,您可能希望缩进它,就好像它是花括号内的代码块一样。
参数实际上可以有一个默认值:
sum<double> S1; // template argument is 'double', EXTRA_PRECISION is false
sum<double, true> S2;
模板可以被看作是一个元函数,它将一组参数映射到一个函数或一个类。例如,sq 模板
template <typename scalar_t>
scalar_t sq(const scalar_t& x);
将类型 T 映射到函数:
T T (*)(const T&)
换句话说,sq 是一个带有 signature double(*)(const double&)的函数。注意 double 是参数 scalar_t 的值。
相反,类模板
template <typename char_t = char>
class basic_string;
将类型 T 映射到类:
T basic_string<T>
有了类,的显式专门化可以限制元功能的域。你有一个通用模板,然后一些专门化;它们中的每一个都可能有也可能没有主体。
// the following template can be instantiated
// only on char and wchar_t
template <typename char_t = char>
class basic_string;
// note: no body
template < >
class basic_string<char>
{ ... };
template < >
class basic_string<wchar_t>
{ ... };
char_t 和 scalar_t 称为模板参数 。当使用 basic_string < char >和 sq < double >时,char 和 double 被称为模板参数 **,**即使 double(sq 的模板参数)和 x(函数 sq < double >的参数)可能会有些混淆。
当您向模板提供模板参数(类型和非类型)时,模板被实例化,因此如果需要,编译器会为模板产生的实体产生机器码。
注意,不同的参数产生不同的实例,即使实例本身是相同的:sq 和 sq 是两个不相关的函数。 4
使用函数模板时,编译器通常会计算出参数。我们说参数将绑定到模板参数。
template <typename scalar_t>
scalar_t sq(const scalar_t& x) { return x*x; }
double pi = 3.14;
sq(pi); // the compiler "binds" double to scalar_t
double x = sq(3.14); // ok: the compiler deduces that scalar_t is double
double x = sq<double>(3.14); // this is legal, but less than ideal
所有模板参数必须是编译时常量。
- 类型参数将接受所有已知的类型。
- 非类型参数按照 most 自动铸造/提升规则工作。 5
以下是一些典型的错误:
template <int N>
class SomeClass
{
};
int main()
{
int A = rand();
SomeClass<A> s; // error: A is not a compile time constant
const int B = rand();
SomeClass<B> s; // error: B is not a compile time constant
static const int C = 2;
SomeClass<C> s; // OK
}
经典 C++ 中编译时常量的最佳语法是 static const[[integer type]]name = value;。
如前所述,如果常量是局部的,在函数体中可以省略静态前缀。然而,它既无害又清晰(您可以通过搜索“static const”而不是单独搜索“const”来找到项目中的所有编译时常量)。 6
传递给模板的参数可以是(编译时)计算的结果。每个有效的整数运算都可以在编译时常数上进行计算:
- 被零除会导致编译器错误。
- 禁止函数调用。 7
- 产生非整数/非指针类型的中间对象的代码是不可移植的,除非在 sizeof: (int)(N*1.2)内部,这是非法的。请改用(N+N/5)。static_cast (0)也可以。 8
SomeClass<(27+56*5) % 4> s1;
SomeClass<sizeof(void*)*CHAR_BIT> s1;
只有当计算完全静态时,被零除才会导致编译器错误。要了解不同之处,请注意这个程序会编译(但不会运行)。
template <int N>
struct tricky
{
int f(int i = 0)
{
return i/N; // i/N is not a constant
}
};
int main()
{
tricky<0> t;
return t.f();
}
test.cpp(5) : warning C4723: potential divide by 0
另一方面,将前面的清单与下面的清单进行比较,其中被零除发生在编译期间(在两种不同的上下文中):
int f()
{
return N/N; // N/N is a constant
}
test.cpp(5) : error C2124: divide or mod by zero
.\test.cpp(5) : while compiling class template member function
'int tricky<N>::f(void)'
with
[
N=0
]
并且具有:
tricky<0/0> t;
test.cpp(12) : error C2975: 'N' : invalid template argument for 'tricky',
expected compile-time constant expression
更准确地说,编译时常数可以是:
-
整数文字,例如 27、CHAR_BIT 和 0x05
-
sizeof 和类似的具有整数结果的非标准语言操作符(例如,alignof where present)
-
非类型模板参数(在“外部”模板的上下文中)
template <int N> class AnotherClass { SomeClass<N> myMember_; };
-
整数类型的静态常数
template <int N, int K> struct MyTemplate { static const int PRODUCT = N*K; }; SomeClass< MyTemplate<10,12>::PRODUCT > s1;
-
一些标准的宏,比如 LINE(其实是有一定自由度的;通常,它们是 long 类型的常量,除了在依赖于实现的“编辑并继续”调试版本中,编译器必须使用引用。在这种情况下,使用宏将导致编译错误。) 9
SomeClass<__LINE__> s1; // usually works...
参数可以依赖于前一个参数:
template
<
typename T,
int (*FUNC)(T) // pointer to function taking T and returning int
>
class X
{
};
template
<
typename T, // here the compiler learns that 'T' is a type
T VALUE // may be ok or not... the compiler assumes the best
>
class Y
{
};
Y<int, 7> y1; // fine
Y<double, 3> y2; // error: the constant '3' cannot have type 'double'
类(和类模板)也可能有模板成员函数 :
// normal class with template member function
struct mathematics
{
template <typename scalar_t>
scalar_t sq(scalar_t x) const
{
return x*x;
}
};
// class template with template member function
template <typename scalar_t>
struct more_mathematics
{
template <typename other_t><sup class="calibre7">10</sup>
static scalar_t product(scalar_t x, other_t y)
{
return x*y;
}
};
double A = mathematics().sq(3.14);
double B = more_mathematics<double>().product(3.14, 5);
1.1.1. 类型名称
使用了关键字 typename :
- 作为类的同义词,在声明类型模板参数时
- 每当编译器看不出标识符是类型名时
对于“不明显”的示例,请考虑以下片段中的 my class:::Y:
template <typename T>
struct MyClass
{
typedef double Y; // Y may or may not be a type
typedef T Type; // Type is always a type
};
template < >
struct MyClass<int>
{
static const int Y = 314; // Y may or may not be a type
typedef int Type; // Type is always a type
};
int Q = 8;
template <typename T>
void SomeFunc()
{
MyClass<T>::Y * Q; // what is this line? it may be:
// the declaration of local pointer-to-double named Q;
// or the product of the constant 314, times the global variable Q
};
y 是一个依赖名,因为它的含义依赖于未知参数 T。
所有直接或间接依赖于未知模板参数的都是依赖名。如果一个依赖名引用一个类型,那么它必须用 typename 关键字引入。
template <typename X>
class AnotherClass
{
MyClass<X>::Type t1_; // error: 'Type' is a dependent name
typename MyClass<X>::Type t2_; // ok
MyClass<double>::Type t3_; // ok: 'Type' is independent of X
};
请注意,在第一种情况下,typename 是必需的,在最后一种情况下是禁止的:
template <typename X>
class AnotherClass
{
typename MyClass<X>::Y member1_; // ok, but it won't compile if X is 'int'.
typename MyClass<double>::Y member2_; // error
};
在声明非类型模板参数时,typename 可能会引入依赖类型:
template <typename T, typename T::type N>
struct SomeClass
{
};
struct S1
{
typedef int type;
};
SomeClass<S1, 3> x; // ok: N=3 has type 'int'
出于好奇,经典的 C++ 标准规定,如果语法 typename T1::T2 在实例化期间产生一个非类型,那么程序就是病态的。然而,它没有指定相反的情况:如果 T1::T2 作为非类型具有有效的含义,那么如果必要的话,它可以在以后被重新解释为类型。例如:
template <typename T>
struct B
{
static const int N = sizeof(A<T>::X);
// should be: sizeof(typename A...)
};
在实例化之前,B“认为”它将在非类型上调用 sizeof 特别是,sizeof 是非类型上的有效运算符,因此代码是合法的。然而,X 可以在以后解析为一个类型,并且代码无论如何都是合法的:
template <typename T>
struct A
{
static const int X = 7;
};
template <>
struct A<char>
{
typedef double X;
};
尽管 typename 的目的是禁止所有这类歧义,但它可能无法涵盖所有的极端情况。 11
1.1.2.尖括号
即使所有参数都有默认值,也不能完全省略尖括号:
template <typename T = double>
class sum {};
sum<> S1; // ok, using double
sum S2; // error
模板参数可能有不同的含义:
- 有时它们确实应该是通用的,例如 std::vector 或 std::set 。可能有一些关于 T 的概念性假设——比如可构造的、可比较的...——不损害普遍性。
- 有时参数被假定属于一个固定的集合。在这种情况下,类模板只是两个或更多类似类的公共实现。 12
在后一种情况下,您可能希望提供一组不带尖括号的常规类,因此您可以从模板基中派生它们,或者只使用 typedef 13 :
template <typename char_t = char>
class basic_string
{
// this code compiles only when char_t is either 'char' or 'wchar_t'
// ...
};
class my_string : public basic_string<>
{
// empty or minimal body
// note: no virtual destructor!
};
typedef basic_string<wchar_t> your_string;
一个流行的编译器扩展(正式成为 C++0x 的一部分)是,两个或多个相邻的“尖括号”将被解析为“模板结束”,而不是“提取操作符”。无论如何,对于旧的编译器,添加额外的空格是一个好习惯:
std::vector<std::list<double>> v1;
// ^^
// may be parsed as "operator>>"
std::vector<std::list<double> > v2;
// ^^^
// always ok
1.1.3.通用构造函数
当处理两个完全相同类型的对象时,不会调用模板复制构造函数和赋值函数:
template <typename T>
class something
{
public:
// not called when S == T
template <typename S>
something(const something<S>& that)
{
}
// not called when S == T
template <typename S>
something& operator=(const something<S>& that)
{
return *this;
}
};
something<int> s0;
something<double> s1, s2;
s0 = s1; // calls user defined operator=
s1 = s2; // calls the compiler generated assignment
自定义模板成员有时被称为通用复制构造函数 和通用赋值 。注意,通用运算符取的是某个东西< X >,而不是 X。
C++ 标准 12.8 说:
- 因为模板构造函数永远不是复制构造函数,所以这种模板的存在不会抑制复制构造函数的隐式声明
- 模板构造函数与其他构造函数(包括复制构造函数)一起参与重载决策,如果模板构造函数比其他构造函数提供更好的匹配,则它可用于复制对象
事实上,在基类中使用非常通用的模板操作符可能会引入错误,如下例所示:
struct base
{
base() {}
template <typename T>
base(T x) {}
};
struct derived : base
{
derived() {}
derived(const derived& that)
: base(that) {}
};
derived d1;
derived d2 = d1;
d2 = d1 的赋值导致堆栈溢出。
隐式复制构造函数必须调用基类的复制构造函数,所以在 12.8 版本中,它不能调用通用构造函数。如果编译器为 derived 生成了一个复制构造函数,它就会调用基复制构造函数(这是隐式的)。可惜给了 derived 的一个复制构造函数,它包含了一个显式的函数调用,即 base(that)。因此,遵循通常的重载决策规则,它匹配 T=derived 的通用构造函数。因为这个函数通过值获取 x,所以它需要执行 x 的一个副本,因此这个调用是递归的。 14
1.1.4.函数类型和函数指针
注意函数类型和指向函数类型的指针之间的区别:
template <double F(int)>
struct A
{
};
template <double (*F)(int)>
struct B
{
};
它们大多是等价的:
double f(int)
{
return 3.14;
}
A<f> t1; // ok
B<f> t2; // ok
通常函数衰减到函数指针就像数组衰减到指针一样。但是函数类型是无法构造的,所以它会导致代码中出现看似无害的故障:
template <typename T>
struct X
{
T member_;
X(T value)
: member_(value)
{
}
};
X<double (int)> t1(f); // error: cannot construct 'member_'
X<double (*)(int)> t2(f); // ok: 'member_' is a pointer
这个问题在返回函子的函数中最为明显(读者可以考虑 std::not1 或者参见第 4.3.4 节)。在 C++ 中,通过引用获取参数的函数模板可以防止衰减:
template <typename T>
X<T> identify_by_val(T x)
{
return X<T>(x);
}
template <typename T>
X<T> identify_by_ref(const T& x)
{
return X<T>(x);
}
double f(int)
{
return 3.14;
}
identify_by_val(f); // function decays to pointer-to-function:
// template instantiated with T = double (*)(int)
identify_by_ref(f); // no decay:
// template instantiated with T = double (int)
对于指针,带有显式参数的函数模板的行为就像普通函数一样:
double f(double x)
{
return x+1;
}
template <typename T>
T g(T x)
{
return x+1;
}
typedef double (*FUNC_T)(double);
FUNC_T f1 = f;
FUNC_T f2 = g<double>;
然而,如果它们是类模板的成员,并且它们的上下文依赖于一个尚未指定的参数,那么它们需要在它们的名字 15 之前有一个额外的模板关键字:
template <typename X>
struct outer
{
template <typename T>
static T g(T x)
{
return x+1;
}
};
template <typename X>
void do_it()
{
FUNC_T f1 = outer<X>::g<double>; // error!
FUNC_T f2 = outer<X>::template g<double>; // correct
}
内部模板类需要 typename 和 template:
template <typename X>
struct outer
{
template <typename T>
struct inner {};
};
template <typename X>
void do_it()
{
typename outer<X>::template inner<double> I;
}
一些编译器在这方面并不严谨。
1.1.5.非模板基类
如果一个类模板有不依赖于它的参数的成员,将它们移动到一个普通的类中可能会很方便:
template <typename T>
class MyClass
{
double value_;
std::string name_;
std::vector<T> data_;
public:
std::string getName() const;
};
应该变成:
class MyBaseClass
{
protected:
~MyBaseClass() {}
double value_;
std::string name_;
public:
std::string getName() const;
};
template <typename T>
class MyClass : MyBaseClass
{
std::vector<T> data_;
public:
using MyBaseClass::getName;
};
派生可以是公共的、私有的,甚至是受保护的。 16 这将降低编译复杂度,并潜在地减少二进制代码的大小。当然,如果模板被实例化多次,这种优化是最有效的。
1.1.6.模板位置
类/函数模板的主体必须在每个实例化点对编译器可用,所以通常的头文件/cpp 文件分离不成立,所有内容都打包在一个文件中,扩展名为 hpp。
如果只有声明可用,编译器将使用它,但链接器将返回错误:
// sq.h
template <typename T>
T sq(const T& x);
// sq.cpp
template <typename T>
T sq(const T& x)
{
return x*x;
}
// main.cpp
#include "sq.h" // note: function body not visible
int main()
{
double x = sq(3.14); // compiles but does not link
如果您只想发布模板的一些实例,单独的头文件会很有用。例如,sq 的作者可能希望分发带有 sq 和 sq 代码的二进制文件,这样它们就是唯一有效的类型。
在 C++ 中,可以在不使用模板实体的情况下,在翻译单元中显式地强制实例化模板实体。这是通过特殊语法实现的:
template class X<double>;
template double sq<double>(const double&);
将这一行添加到 sq.cpp 将“导出”sq ,就像它是一个普通的函数一样,并且 sq.h 的简单包含将足以构建该程序。
这个特性通常与算法标签一起使用。假设你有一个函数模板,比如加密或压缩,它的算法细节必须保密。模板参数 T 代表一个小集合中的一个选项(比如 T =快速、正常、最好);显然,该算法的用户不应该添加他们自己的选项,所以您可以强制实例化少量的实例——加密、加密和加密——并且只分发一个头文件和一个二进制文件。
注意 C++0x 给语言增加了模板的外部实例化。如果在 template 之前使用关键字 extern,编译器将跳过实例化,链接器将从另一个翻译单元借用模板体。
另见下文第 1.6.1 节。
1.2.专门化和论证演绎
根据定义,当名称出现在名称空间、类或函数体的花括号之间时,我们说名称在 名称空间级别 为*,在类级别 *,或在**主体级别,如下例所示:**
class X // here, X is at namespace level
{
public:
typedef double value_type; // value_type is at class level
X(const X& y) // both X and y are at class level
{
}
void f() // f is at class level
{
int z = 0; // body level
struct LOCAL {}; // LOCAL is a local class
}
};
函数模板——成员或非成员——可以通过查看它们的参数列表自动推导出模板参数。大致来说, 17 编译器会挑选与参数最匹配的专用函数。如果可行,最好是完全匹配,但也可能发生转换。
如果你可以用调用 G 来代替对 F 的任何调用,那么函数 F 比 G 更专门化(在相同的参数上),但反之则不然。此外,非模板函数被认为比同名模板函数更专门化。
有时候超载和特殊化看起来非常相似:
template <typename scalar_t>
inline scalar_t sq(const scalar_t& x); // (1) function template
inline double sq(const double& x); // (2) overload
template <>
inline int sq(const int& x); // (3) specialization of 1
但它们并不完全相同;考虑以下反例:
inline double sq(float x); // ok, overloaded sq may
// have different signature
template <> // error: invalid specialization
inline int sq(const int x); // it must have the same signature
重载和专门化之间的基本区别在于,函数模板充当单个实体,而不管它有多少个专门化。例如,紧接在(3)之后的调用 sq(y)将迫使编译器在实体(1)和(2)之间进行选择。如果 y 是 double,那么(2)是首选,因为它是一个正常的函数;否则,(1)基于 y 的类型被实例化:只有在这一点上,如果 y 碰巧是 int,编译器才会注意到 sq 有一个专门化并选择(3)。
请注意,两个不同的模板可能会占据主导地位:
template <typename T>
void f(const T& x)
{
std::cout << "I am f(reference)";
}
或者:
template <typename T>
void f(const T* x)
{
std::cout << "I am f(pointer)";
}
另一方面,当存在重载模板时,编写专门化可能需要您显式指定参数:
template <typename T> void f(T) {}
template <typename T> void f(T*) {}
template <>
void f(int*) // ambiguous: may be the first f with T=int*
{} // or the second with T=int
template <>
void f<int>(int*) // ok
{}
记住,模板专门化只在名称空间级别是合法的(即使大多数编译器会容忍它):
class mathematics
{
template <typename scalar_t>
inline scalar_t sq(const scalar_t& x) { ... }; // template member function
template <>
inline int sq(const int& x) { ... }; // illegal specialization!
};
标准的方法是从类内部调用全局函数模板:
// global function template: outside
template <typename scalar_t>
inline scalar_t gsq(const scalar_t& x) { ... };
// specialization: outside
template <>
inline int gsq(const int& x) { ... };
class mathematics
{
// template member function
template <typename scalar_t>
inline scalar_t sq(const scalar_t& x)
{
return gsq(x);
}
};
有时您可能需要显式指定模板参数,因为它们与函数实参无关(实际上,它们被称为非 可演绎 ):
class crc32 { ... };
class adler { ... };
template <typename algorithm_t>
size_t hash_using(const char* x)
{
// ...
}
size_t j = hash_using<crc32>("this is the string to be hashed");
在这种情况下,您必须首先放置不可推导的类型和参数,这样编译器就可以计算出所有剩余的类型和参数:
template <typename algorithm_t, typename string_t>
int hash_using(const string_t& x);
std::string arg("hash me, please");
int j = hash_using<crc32>(arg); // ok: algorithm_t is crc32
// and string_t is std::string
参数演绎显然只对函数模板成立,对类模板不成立。
明确地提供一个论点,而不是依赖于演绎,通常不是一个好主意,除非在一些特殊的情况下,这将在下面描述。
-
必要时进行消歧 :
template <typename T> T max(const T& a, const T& b) { ... } int a = 7; long b = 6; long m1 = max(a, b); // error: ambiguous, T can be int or long long m2 = max<long>(a, b); // ok: T is long
-
当类型不可演绎时 18 :
template <typename T> T get_random() { ... } double r = get_random<double>();
-
当你想让一个函数模板看起来像一个内置的 C++ cast 操作符 :
template <typename X, typename T> X sabotage_cast(T* p) { return reinterpret_cast<X>(p+1); } std::string s = "don't try this at home"; double* p = sabotage_cast<double*>(&s);
-
同时执行造型和函数模板调用 :
double y = sq<int>(6.28) // casts 6.28 to int, then squares the value
-
当一个算法有一个参数,它的默认值是依赖于模板的(通常是一个函子) 19 :
template <typename LESS_T> void nonstd_sort (..., LESS_T cmp = LESS_T()) { // ... } // call function with functor passed as template argument nonstd_sort< std::less<...> > (...); // call function with functor passed as value argument nonstd_sort (..., std::less<...>());
一个模板名(比如 std::vector)和它生成的类名(比如 std::vector < int >)不一样。在类级别,它们是等效的:
template <typename T>
class something
{
public:
something() // ok: don't write something<T>
{
// at local level, 'something' alone is illegal
}
something(const something& that); // ok: 'something&' stands for
// 'something<T>&'
template <typename other_t>
something(const something<other_t>& that)
{
}
};
通常,没有尖括号的单词 something 单独代表一个模板,它本身就是一个定义明确的实体。在 C++ 中,有模板——模板参数 。您可以声明一个模板,其参数不仅是类型,而且是匹配给定模式的类模板:
template <template <typename T> class X>
class example
{
X<int> x1_;
X<double> x2_;
};
typedef example<something> some_example; // ok: 'something' matches
注意,class 和 typename 在这里是不等价的:
template <template <typename T> typename X> // error
类模板可以全部或部分专门化。在通用模板之后,我们列出了专用版本:
// in general T is not a pointer
template <typename T>
struct is_a_pointer_type
{
static const int value = 1;
};
// 2: full specialization for void*
template <>
struct is_a_pointer_type<void*>
{
static const int value = 2;
};
// 3: partial specialization for all pointers
template <typename X>
struct is_a_pointer_type<X*>
{
static const int value = 3;
};
int b1 = is_a_pointer_type<int*>::value; // uses 3 with X=int
int b2 = is_a_pointer_type<void*>::value; // uses 2
int b3 = is_a_pointer_type<float>::value; // uses the general template
部分专门化可以是递归的:
template <typename X>
struct is_a_pointer_type<const X>
{
static const int value = is_a_pointer_type<X>::value;
};
下面的例子被称为*指针悖论* :
#include <iostream>
template <typename T>
void f(const T& x)
{
std::cout << "My arg is a reference";
}
template <typename T>
void f(const T* x)
{
std::cout << " My arg is a pointer";
}
事实上,下面的代码会像预期的那样打印出来:
const char* s = "text";
f(s);
f(3.14);
My arg is a pointer
My arg is a reference
现在改为写:
double p = 0;
f(&p);
你应该读指针;取而代之的是,你得到了对第一个重载的调用。编译器是正确的,因为类型 double通过一个普通的隐式转换(即添加 const-ness)匹配 const T,但它完美地匹配 const T T&,设置 T=double*。
1.2.1.扣除
函数模板可以推导出它们的参数,将参数类型与它们的签名相匹配:
template <typename T>
struct arg;
template <typename T>
void f(arg<T>);
template <typename X>
void g(arg<const X>);
arg<int*> a;
f(a); // will deduce T = int*
arg<const int> b;
f(b); // will deduce T = const int
g(b); // will deduce X = int
演绎也包括非类型参数:
template < int I>
struct arg;
template <int I>
arg<I+1> f(arg<I>);
arg<3> a;
f(a); // will deduce I=3 and thus return arg<4>
但是,请记住,演绎是通过“模式匹配”完成的,编译器不需要执行任何类型的代数 20 :
// this template is formally valid, but deduction will never succeed...
template <int I>
arg<I> f(arg<I+1>)
{
// ...
}
arg<3> a;
f(a); // ...the compiler will not solve the equation I+1==3
arg<2+1> b;
f(b); // ...error again
No matching function for call to 'f'
Candidate template ignored: couldn't infer template argument 'I'
另一方面,如果一个类型包含在一个类模板中,那么它的上下文(外部类的参数)不能被推导出来:
template <typename T>
void f(typename std::vector<T>::iterator);
std::vector<double> v;
f(v.begin()); // error: cannot deduce T
注意,这个错误不依赖于特定的调用。这种推演在逻辑上是不可能的;t 可能不是唯一的。
template <typename T>
struct A
{ typedef double type; };
// if A<X>::type is double, X could be anything
可以添加伪参数来加强一致性:
template <typename T>
void f(std::vector<T>&, typename std::vector<T>::iterator);
编译器将从第一个参数中推导出 T,然后验证第二个参数的类型是否正确。
您也可以在调用函数时显式提供 T 的值:
template <typename T>
void f(typename std::vector<T>::iterator);
std::vector<double> w;
f<double>(w.begin());
经验表明,最好尽量少用没有推导出参数的函数模板。自动推导通常会给出更好的错误信息和更容易的函数查找;以下部分列出了一些常见情况。
首先,当使用模板语法调用函数时,编译器不一定要寻找模板。这可能会产生模糊的错误消息。
struct base
{
template <int I, typename X> // template, where I is non-deduced
void foo(X, X)
{
}
};
struct derived : public base
{
void foo(int i) // not a template
{
foo<314>(i, i); // line #13
}
};
1>error: 'derived::foo': function call missing argument list; use '&derived::foo' to create a pointer to member
1>error: '<' : no conversion from 'int' to 'void (__cdecl derived::* )(int)'
1> There are no conversions from integral values to pointer-to-member values
1>error: '<' : illegal, left operand has type 'void (__cdecl derived::* )(int)'
1>warning: '>' : unsafe use of type 'bool' in operation
1>warning: '>' : operator has no effect; expected operator with side-effect
当编译器遇到 foo <314>时,它会寻找任何 foo。derived 中的第一个匹配是 void foo(int ),查找停止。于是,foo <314>被曲解为(普通函数名) (少)(314)(大)。代码应该显式指定 base::foo。
第二,如果名称查找成功并有多个结果,则显式参数约束重载决策:
template <typename T>
void f();
template <int N>
void f();
f<double>(); // invokes the first f, as "double" does not match "int N"
f<7>(); // invokes the second f
然而,这可能会引起意想不到的麻烦,因为有些过载 21 可能会被默默忽略:
template <typename T>
void g(T x);
double pi = 3.14;
g<double>(pi); // ok, calls g<double>
template <typename T>
void h(T x);
void h(double x);
double pi = 3.14;
h<double>(pi); // unexpected: still calls the first h
这是另一个例子:
template <int I>
class X {};
template <int I, typename T>
void g(X<I>, T x);
template <typename T> // a special 'g' for X<0>
void g(X<0>, T x); // however, this is g<T>, not g<0,T>
double pi = 3.14;
X<0> x;
g<0>(x, pi); // calls the first g
g(x, pi); // calls the second g
最后但并非最不重要的一点是,旧编译器过去常常会引入微妙的链接器错误(比如调用错误的函数)。
1.2.2.特化作用
模板专门化只在名称空间级别有效 22 :
struct X
{
template <typename T>
class Y
{};
template <> // illegal, but usually tolerated by compilers
class Y<double>
{};
};
template <> // legal
class X::Y<double>
{
};
编译器只有在编译了专用版本之后才会开始使用它:
template <typename scalar_t>
scalar_t sq(const scalar_t& x)
{ ... }
struct A
{
A(int i = 3)
{
int j = sq(i); // the compiler will pick the generic template
}
};
template <>
int sq(const int& x) // this specialization comes too late, compiler gives error
{ ... }
但是,在这种情况下,编译器会给出一个错误(声明专门化在实例化之后)。顺便提一下,泛型类模板可能会明确地“提到”一个特例,作为某个成员函数中的一个参数。下面的代码实际上导致了前面提到的编译器错误。
template <typename T>
struct C
{
C(C<void>)
{
}
};
template <>
struct C<void>
{
};
正确的版本使用了正向声明 :
template <typename T>
struct C;
template <>
struct C<void>
{
};
template <typename T>
struct C
{
C(C<void>)
{
}
};
注意,您可以使用整数模板参数来部分专门化(并且您会经常这样做):
// general template
template <typename T, int N>
class MyClass
{ ... };
// partial specialization (1) for any T with N=0
template <typename T>
class MyClass<T, 0>
{ ... };
// partial specialization (2) for pointers, any N
template <typename T, int N>
class MyClass<T*, N>
{ ... };
然而,这种方法可能会引入歧义:
MyClass<void*, 0> m; // compiler error:
// should it use specialization (1) or (2)?
通常你必须明确列出所有的“组合”。如果你为所有 T1 ∈ A 和所有 T2 ∈ B 专门化 X ,那么你也必须显式专门化 X ∈ A×B
// partial specialization (3) for pointers with N=0
template <typename T>
class MyClass<T*, 0>
{ ... };
当通用模板中的模板参数之间存在依赖关系时,编写部分专门化是非法的。
// parameters (1) and (2) are dependent in the general template
template <typename int_t, int_t N>
class AnotherClass
{};
template <typename T>
class AnotherClass<T, 0>
{};
error: type 'int_t' of template argument '0' depends on template parameter(s)
只允许完全专门化:
template <>
class AnotherClass<int, 0>
{};
类模板特殊化可能与通用模板完全无关。它不需要有相同的成员,成员函数可以有不同的签名。
虽然不必要的界面改变是不良风格的征兆(因为它禁止对对象的任何一般操作),但这种自由通常可以被利用:
template <typename T, int N>
struct base_with_array
{
T data_[N];
void fill(const T& x)
{
std::fill_n(data_, N, x);
}
};
template <typename T>
struct base_with_array<T, 0>
{
void fill(const T& x)
{
}
};
template <typename T, size_t N>
class cached_vector : private base_with_array<T, N>
{
// ...
public:
cached_vector()
{
this->fill(T());
}
};
1.2.3.内部类模板
一个类模板可以是另一个模板的成员。其中一个重点是语法;内部类有自己的一组参数,但它知道外部类的所有参数。
template <typename T>
class outer
{
public:
template <typename X>
class inner
{
// use freely both X and T
};
};
如果 T 是明确定义的类型,则访问 inner 的语法是 outer:inner;如果 T 是一个模板参数,你就要写外::模板内:
outer<int>::inner<double> a; // correct
template <typename Y>
void f()
{
outer<Y>::inner<double> x1; // error
outer<Y>::template inner<double> x1; // correct
}
通常很难或者不可能专门化内部类模板。专门化应该在 outer 之外列出,所以通常它们需要两个模板<...>子句,前者用于 T (outer),后者用于 X (inner)。
| 主模板:它定义了一个内部的,我们非正式地称之为 inner_1。 | 模板外部类{模板内部类{};}; | | outer 的完全专门化可能包含一个 inner ,它对编译器来说与 inner_1 完全无关;我们把这个叫做 inner_2。 | 模板<>班外{模板内部类{//好的};}; | | inner_2 可以是专用的: | 模板<>类外部:内部{//好的}; | | 固定 T (=double)和一般 x 的 inner_1 的特化。 | 模板<>模板类外部::内部{//好的}; | | inner_1 对于固定 T (=double)和固定 X (=char)的特化。 | 模板<>模板<>类外部:内部{//好的}; | | 用任意 t 对固定 X 进行 inner_1 特殊化是非法的。 | 模板模板<>类外部:内部{//错误!}; |
注意,即使 X 相同,inner_1 和 inner_2 是完全不同的类型:
template <typename T>
struct outer
{
template <typename X> struct inner {};
};
template <>
struct outer<int>
{
template <typename X> struct inner {};
};
int main()
{
outer<double>::inner<void> I1;
outer<int>::inner<void> I2;
I1 = I2;
}
error: binary '=' : no operator found which takes a right-hand operand of type 'outer<int>::inner<X>' (or there is no acceptable conversion)
比方说,不可能编写一个函数来测试任意两个“inner”是否相等,因为给定 inner 的一个实例,编译器不会推导出它的 outer 。
template <typename T, typename X>
bool f(outer<T>::inner<X>); // error: T cannot be deduced?
变量 I1 的实际类型不是简单的内,而是外:【内。如果对于任何 X,所有 inner 都应该有相同的类型,那么 inner 必须提升为全局模板。如果它是一个简单的类,它会简单地产生:
struct basic_inner
{
};
template <typename T>
struct outer
{
typedef basic_inner inner;
};
template <>
struct outer<int>
{
typedef basic_inner inner;
};
如果 inner 不依赖于 T,你可以写 23 :
template <typename X>
struct basic_inner
{
};
template <typename T>
struct outer
{
template <typename X>
struct inner : public basic_inner<X>
{
inner& operator=(const basic_inner<X>& that)
{
static_cast<basic_inner<X>&>(*this) = that;
return *this;
}
};
};
template <>
struct outer<int>
{
template <typename X>
struct inner : public basic_inner<X>
{
inner& operator=(const basic_inner<X>& that)
{
static_cast<basic_inner<X>&>(*this) = that;
return *this;
}
};
};
否则,您必须设计支持混合操作的 basic_inner 模板操作符:
template <typename X, typename T>
struct basic_inner
{
template <typename T2>
basic_inner& operator=(const basic_inner<X, T2>&)
{ /* ... */ }
};
template <typename T>
struct outer
{
template <typename X>
struct inner : public basic_inner<X, T>
{
template <typename ANOTHER_T>
inner& operator=(const basic_inner<X, ANOTHER_T>& that)
{
static_cast<basic_inner<X, T>&>(*this) = that;
return *this;
}
};
};
template <>
struct outer<int>
{
template <typename X>
struct inner : public basic_inner<X, int>
{
template <typename ANOTHER_T>
inner& operator=(const basic_inner<X, ANOTHER_T>& that)
{
static_cast<basic_inner<X, int>&>(*this) = that;
return *this;
}
};
};
int main()
{
outer<double>::inner<void> I1;
outer<int>::inner<void> I2;
I1 = I2; // ok: it ends up calling basic_inner::operator=
}
这在 C++ 社区中被称为可怕的初始化。 24
吓人代表*“看似错误(受冲突的模板参数约束),但实际上与正确的实现一起工作”*。简单地说,两个应该不同的内部类型(具体来说,外部< T1 >:“内部”和外部< T2 >:“内部”)实际上共享了实现,这意味着可以将它们统一视为“两个内部”。
正如您看到的函数模板,在编译器满足所有专门化之前,您永远不应该实例化主模板。如果只使用完全专门化,编译器会识别出问题并停止。太迟的部分专业化将被忽略:
struct A
{
template <typename X, typename Y>
struct B
{
void do_it() {} // line #1
};
void f()
{
B<int,int> b; // line #2: the compiler instantiates B<int,int>
b.do_it();
}
};
template <typename X>
struct A::B<X, X> // this should be a specialization of B<X,X>
// but it comes too late for B<int,int>
{
void do_it() {} // line #3
};
A a;
a.f(); // calls do_it on line #1
此外,添加 B 的完全专门化将触发编译器错误:
template <>
struct A::B<int, int>
{
void do_it() {}
};
error: explicit specialization; 'A::B<X,Y>' has already been instantiated
with
[
X=int,
Y=int
]
显而易见的解决方案是在 A::B 的专门化之后移动函数体。
1.3.风格惯例
风格是编写代码的方式;这个定义非常模糊,它包括了编程的许多不同方面,从语言技术到花括号的位置。
名称空间 std 中的所有 C++ 对象都展示了一种通用的风格,这使得库更加一致。
比如所有的名字都是小写 25 多字名字用下划线。容器有一个测试对象是否为空的成员函数 bool T::empty() const 和一个使容器为空的 void T::clear() 。这些都是风格的元素。
一个用纯 C 写的虚构的 STL 可能有一个全局函数 clear,为所有可能的容器重载。编写像 cont.clear()或 clear(&cont)这样的代码对 cont 有相同的净影响,甚至可能生成相同的二进制文件,但是,它有非常不同的风格。
所有这些方面在代码评审期间都很重要。如果风格与读者的格式一致,代码看起来自然清晰,维护也更容易。
风格的某些方面确实不太重要,因为它们很容易调整。例如,使用美化器——团队中的每个工作人员都可能在他的机器上有一个预配置的美化器,它与代码编辑器集成在一起,可以快速地重新格式化大括号、空格和换行符。
注意 JEdit(参见http://www.jedit.org)是一个支持插件的免费多平台代码编辑器。
AStyle(艺术风格)是一个命令行开源代码美化器(见http://astyle.sourceforge.net),其首选项包括最常见的格式化选项(见图 1-1 )。
图 1-1 。JEdit 的风格插件
大多数合理的风格约定是等价的;重要的是选择一个,并尝试在一段时间内保持一致。 26
理想情况下,如果代码是根据一些常见的行为惯例编写的,读者可能会根据风格推断出它是如何工作的,而不需要查看细节。
例如:
void unknown_f(multidimensional_vector<double, 3, 4>& M)
{
if (!M.empty())
throw std::runtime_error("failure");
}
大多数读者会把这个片段描述为“如果多维向量不为空,那么抛出一个异常”。然而,除了样式之外,代码中没有任何内容表明这是预期的行为。
事实上,多维向量::empty 原则上可以使容器为空,如果不成功,则返回一个非零错误代码。 27
命名惯例是风格的一个重要组成部分。
下面的例子列出了在构建对象名称时如何传达额外含义的一些想法。它不是一套公理,特别是没有一项比它的对立面更差/更好,但它是一个如何组合一种风格的详细示例,可以帮助您诊断和解决问题。
请记住,C++ 标准规定,有些标识符是“为任何用途的实现保留的”,有些是为全局或 std 名称空间中的名称保留的。这意味着用户名永远不应该:
- 以下划线开头(特别是后面跟着一个大写字母)
- 包含双下划线
- 包含一个美元符号(一些编译器可以接受,但是不可移植)
1.3.1.评论
许多好的编程实践都可以归结为为变化做准备或表达意图。新手强调前者,专家强调后者。”
约翰·库克
记得在你的代码中添加大量的注释。如果这对于任何编程语言都是有效的,那么对于 TMP 技术尤其如此,这很容易被误解。TMP 的正确行为基于奇怪的实体,比如空类、void 函数和看起来像错误的奇怪语言结构。对于代码的作者来说,要记住这些技术为什么以及如何工作真的很难,对于其他必须维护代码的人来说就更难了。
1.3.2.宏指令
宏在 TMP 中起着特殊的作用。一些程序员认为它们是必要的邪恶,事实上它们是必要的,但是它们也是邪恶的并不明显。
宏必须:
- 让读者认识到它们
- 防止名称冲突
满足这两个要求的最简单的方法是为所有宏选择一个唯一且足够难看的公共前缀,并使用小写/大写字母赋予名称额外的含义。
例如,您可能同意所有宏都以 MXT_ 开头。如果宏是持久的,即从不未定义,前缀将是 MXT 。如果宏的作用域是有限的(在同一个文件中它被定义和未定义),前缀将是 mXT_。
#ifndef MXT_filename_
#define MXT_filename_ // this is "exported" – let's name it MXT_*
#define mXT_MYVALUE 3 // this macro has limited "scope"
const int VALUE = mXT_MYVALUE; // let's name it mXT_*
#undef mXT_MYVALUE //
#endif //MXT_filename_
小写前缀 mxt 用于重新映射不同平台中的标准/系统函数名:
#ifdef _WIN32
#define mxt_native_dbl_isfinite _finite
#else
#define mxt_native_dbl_isfinite isfinite
#endif
为了获得更好的代码外观,您可以决定用宏替换一些关键字:
#define MXT_NAMESPACE_BEGIN(x) namespace x {
#define MXT_NAMESPACE_END(x) }
#define MXT_NAMESPACE_NULL_BEGIN() namespace {
#define MXT_NAMESPACE_NULL_END() }
和/或将名称空间指令包含在 ASCII-art 注释框中:
/////////////////////////////////////////////////////////////////
MXT_NAMESPACE_BEGIN(XT)
/////////////////////////////////////////////////////////////////
将一些(整数)函数作为一组宏是有用的:
#define MXT_M_MAX(a,b) ((a)<(b) ? (b) : (a))
#define MXT_M_MIN(a,b) ((a)<(b) ? (a) : (b))
#define MXT_M_ABS(a) ((a)<0 ? -(a) : (a))
#define MXT_M_SQ(a) ((a)*(a))
infix M 代表“宏”,在使用模板时会很有用:
template <int N>
struct SomeClass
{
static const int value = MXT_M_SQ(N)/MXT_M_MAX(N, 1);
};
注c++ 11 标准引入了一个新的关键字:constexpr 。 28
函数声明的 constexpr 没有副作用,它总是从相同的参数返回相同的结果。特别是,当使用编译时常量参数调用这样的函数时,它的结果也将是一个编译时常量:
constexpr int sq(int n) { return n*n; }
constexpr int max(int a, int b)
{ return a<b ? b : a; }
template <int N>
struct SomeClass
{
static const int value = sq(N)/max(N, 1);
最后,考虑一类特殊的宏。一个 宏指令 是一个宏,其用法逻辑上需要一整行代码。
换句话说,普通宏和指令的区别在于,后者不能与同一行上的任何东西共存(可能除了它的参数):
// directive
MXT_NULL_NAMESPACE_BEGIN()
#define MXT_PI 3.1415926535897932384626433832795029
// the use of MXT_PI does not take the whole line
// so it is not a directive.
const double x = std::cos(MXT_PI);
// directive
MXT_NULL_NAMESPACE_END()
一般来说,宏指令的定义不应该以分号结束,所以用户被迫手动关闭该行(在适当的时候),就像它是一个标准的函数调用一样。
// note: no trailing ';'
#define MXT_INT_I(k) int i = (k)
int main()
{
MXT_INT_I(0); // put ';' here
return 0;
}
这里有一个更复杂的例子。注意,结尾的分号是一个非常强的样式点,所以它甚至用在普通代码中分号不自然的地方。
#define mXT_C(NAME,VALUE) \
static scalar_t NAME() \
{ \
static const scalar_t NAME##_ = (VALUE); \
return NAME##_; \
}
template <typename scalar_t>
struct constant
{
// the final ';' at class level is legal, though uncommon
mXT_C(Pi, acos(scalar_t(-1)));
mXT_C(TwoPi, 2*acos(scalar_t(-1)));
mXT_C(PiHalf, acos(scalar_t(0)));
mXT_C(PiQrtr, atan(scalar_t(1)));
mXT_C(Log2, log(scalar_t(2)));
};
#undef mXT_C
double x = constant<double>::TwoPi();
但是,调用宏指令时需要特别小心,宏指令会扩展为一系列指令:
#define MXT_SORT2(a,b) if ((b)<(a)) swap((a),(b))
#define MXT_SORT3(a,b,c) \
MXT_SORT2((a),(b)); MXT_SORT2((a),(c)); MXT_SORT2((b),(c))
int a = 5, b = 2, c = 3;
MXT_SORT3(a,b,c); // apparently ok: now a=2, b=3, c=5
然而,这个代码被破坏了:
int a = 5, b = 2, c = 3;
if (a>10)
MXT_SORT3(a,b,c); // problem here!
因为它扩展到:
if (a>10)
MXT_SORT2(a,b);
MXT_SORT2(a,c);
MXT_SORT2(b,c);
更令人惊讶的是,下面的片段很清楚,但不正确:
if (a>10)
MXT_SORT2(a,b);
else
MXT_SORT2(c,d);
由于 if-then-else 在 C++ 中的关联方式,宏扩展为
if (a>10)
if (a<b)
swap(a,b);
else
if (c<d)
swap(c,d);
缩进不像代码执行的方式;该块实际上分为
if (a>10)
{
if (a<b)
swap(a,b);
else if (c<d)
swap(c,d);
}
要解决这个问题,您可以使用 do {...} while(假)成语:
#define MXT_SORT3(a,b,c) \
do { MXT_SORT2((a),(b)); MXT_SORT2((a),(c)); MXT_SORT2((b),(c)); } \
while (false)
这既允许将“本地代码”放入块中,也允许用分号结束指令。
请记住,这不会让您免于类似以下的错误:
MXT_SORT3(a, b++, c); // error: b will be incremented more than once
这就是为什么我们坚持认为宏可以通过一个“足够难看”的前缀立即识别出来。
要解决“如果”的宏问题,编写一个不做其他事情的分支:
#define MXT_SORT2(a,b) if ((b)<(a)) swap((a),(b)); else
现在 MXT_SORT2(a,b);扩展到 if(...)互换(...);否则;其中最后一个分号是空语句。更好的 29 :
#define MXT_SORT2(a,b) if (!((b)<(a))) {} else swap((a),(b))
最后一点,永远不要直接使用来自宏的类型。总是引入 typedef。如果没有仔细编写宏,*和 const 之间的关联可能会产生意外的结果。考虑一下:
T x = 0;
const T* p = &x; // looks correct
除非:
#define T char*
相反,考虑截取宏:
typedef T MyType; // ok, even if T is a macro.
// #undef T if you like
MyType x = 0; //
const MyType* p = &x; // now it works.
1.3.3.符号
大多数 C++ 项目包含几种符号(类、函数、常数等等)。可以在系统/框架实用程序(完全抽象和通用)和项目特定实体(包含特定逻辑并且不期望在其他地方重用)之间画一条粗略的分界线。
这个简单的分类可能对(人类)调试器很重要。如果任何一段代码被认为是“系统实用程序”,那么它是绝对可信的,在调试过程中通常会被“忽略”。另一方面,特定于项目的代码可能较少被测试,应该“介入”。
我们可以同意稳定符号应该遵循 STL 命名约定(小写,下划线,比如 stable_sort,hash_map 等等)。这通常是类模板的情况。
剩下的应该是 camel case(Java 约定就可以了)。
(framework header) sq.hpp
template <typename scalar_t>
scalar_t sq(const scalar_t& x) { return x*x; }; // 'system-level' function – lowercase
(project file) custom_scalar.h
struct MySpecialScalarType // 'project-level' class – mixed case
{
// ...
};
(project file) main.cpp
int main()
{
MySpecialScalarType x = 3.14;
MySpecialScalarType y = sq(x);
return 0;
}
一个仿函数 是一个对象的实例,它实现了至少一个运算符(),因此实例的名字就像一个函数。 30
如果函子通过非常数引用接受参数,则称其为修改。
一个谓词是一个非修改函子,它接受相同类型的所有参数并返回一个布尔值。例如,less 是一个二元谓词:
template <typename T>
struct less
{
bool operator()(const T&, const T&) const;
};
大多数仿函数包含运算符()的返回类型的 typedef,通常命名为 result_type 或 value_type。 31
函子通常是无状态的,或者它们携带很少的数据成员,所以它们是动态构建的。有时,您可能需要一个有意义的实例名,但这可能不太容易,因为如果仿函数有一个有限的“作用域”,那么唯一有意义的名称已经给了类。
calendar myCal;
std::find_if(year.begin(), year.end(), is_holiday(myCal));
// is_holiday is a class
// how do we name an instance?
您可以使用下列选项之一:
-
对于实例:
calendar myCal; is_holiday IS_HOLIDAY(myCal); std::find_if(year.begin(), year.end(), IS_HOLIDAY);
,使用小写的仿函数名并将其转换为大写
-
使用带有前缀/后缀的小写仿函数名,并在实例中删除它:
calendar myCal; is_holiday_t is_holiday(myCal); std::find_if(year.begin(), year.end(), is_holiday);
1.3.4.概括性
提高通用性的最好方法是重用标准类,比如 std::pair。
这带来了经过良好测试的代码,增加了互操作性;但是,它可能经常隐藏一些特定的逻辑,例如 pair::first 和 pair::second 的含义可能乍一看并不明显。请看下面的典型例子:
struct id_value
{
int id;
double value;
};
id_value FindIDAndValue(...);
这可以替换为:
std::pair<int, double> FindIDAndValue(...)
但是,第一个函数的调用者可以编写 p.id 和 p.value,这比 p.first 和 p.second 更容易阅读。您可能希望提供一种不太通用的方法来访问 pair 成员:
-
宏指令
#define id first // bad idea? #define value second // bad idea? #define id(P) P.first // slightly better #define value(P) P.second // slightly better
-
全局函数(这些被称为访问器;参见第 6.2.1 节
inline int& id(std::pair<int, double>& P) { return P.first; } inline int id(const std::pair<int, double>& P) { return P.first; }
-
成员的全局指针
typedef std::pair<int, double> id_value; int id_value::*ID = &id_value::first; double id_value::*VALUE = &id_value::second; // later std::pair<int, double> p; p.*ID = -5; p.*VALUE = 3.14;
要使 ID 和值成为常量,语法是:
int id_value::* const ID = &id_value::first;
1.3.5.模板参数
一个相当普遍接受的惯例是为非类型模板参数保留大写名称。这可能会导致与宏的某些名称冲突。并不总是需要给模板参数起一个名字(就像函数参数一样),所以如果可行的话,最好完全去掉这个名字:
// the following line is likely to give strange errors
// since some compilers define BIGENDIAN as a macro!
template <typename T, bool BIGENDIAN = false>
class SomeClass
{
};
template <typename T>
class SomeClass<T, true>
{
};
更安全的声明应该是 32 :
template <typename T, bool = false>
class SomeClass
类型参数通常由一个大写字母表示,通常是 T(或 T1,T2...)如果类型确实可以是任何东西。 33 A 和 R 传统上也用于匹配自变量和结果的参数:
int foo(double x) { return 5+x; }
template <typename R, typename A>
inline R apply(R (*F)(A), A arg)
{
return F(arg);
}
template <typename R, typename A1, typename A2>
inline R apply(R (*F)(A1, A2), A1 arg1, A2 arg2)
{
return F(arg1, arg2);
}
double x = apply(&foo 3.14);
否则,您可能希望使用以 _t 结尾的(有意义的)小写名称(例如,int_t、scalar_t、object_t、any_t 或 that_t)。
template <typename T, int N>
class do_nothing
{
};
template <typename int_t> // int_t should behave as an integer type<sup class="calibre7">34</sup>
struct is_unsigned
{
static const bool value = ...;
};
后缀 _t 在 C 语言中最初的意思是 typedef,它也广泛用于代表模板实例的(私有)typedef:
template <typename scalar_t>
class SomeContainer
{
// informally means:
// within this class, a pair always denotes a pair of scalars
private:
typedef std::pair<scalar_t, scalar_t> pair_t;
};
另一方面,公共 typedef 名称通常由小写的常规英语单词组成(例如 iterator_category)。在这种情况下,_type 是首选:
template <typename scalar_t>
class SomeContainer
{
public:
typedef scalar_t result_type;
};
1.3.6.元函数
我们经常会遇到无状态的类模板,它的成员只有枚举(通常是匿名的)、静态常量、类型(typedefs 或嵌套类)和静态成员函数。
概括第 1.1 节,我们认为这个模板是一个元函数,它将它的参数元组映射到一个类,这个类被看作是一个结果集(即它的成员)。
template <typename T, int N>
struct F
{
typedef T* pointer_type;
typedef T& reference_type;
static const size_t value = sizeof(T)*N;
};
元函数 F 将一对参数映射到三个结果:
| (T,N) | →中 | (指针类型,引用类型,值) | | {type}×{int} | →中 | {type}×{type}×{size_t} |
大多数元函数要么返回单个类型,即通常命名的类型,要么返回单个数值常量(整数或枚举),即通常命名的值。35T3】
template <typename T>
struct largest_precision_type;
template <>
struct largest_precision_type<float>
{
typedef double type;
};
template <>
struct largest_precision_type<double>
{
typedef double type;
};
template <>
struct largest_precision_type<int>
{
typedef long type;
};
类似地:
template <unsigned int N>
struct two_to
{
static const unsigned int value = (1<<N);
};
template <unsigned int N>
struct another_two_to
{
enum { value = (1<<N) };
};
unsigned int i = two_to<5>::value; // invocation
largest_precision<int>::type j = i + 100; // invocation
历史上,第一个元函数是使用枚举编写的:
template <size_t A>
struct is_prime
{
enum { value = 0 };
};
template <>
struct is_prime<2>
{
enum { value = 1 };
};
template <>
struct is_prime<3>
{
enum { value = 1 };
};
// ...
主要原因是编译器不能处理静态常量整数(包括 bool)。与静态常量相比,使用枚举的优势在于编译器不会为常量保留存储空间,因为计算要么是静态的,要么会失败。
相反,静态常量整数可能被“误用”为普通整数,例如,取其地址(编译器在枚举上不允许的操作)。
注意根据经典的 C++ 标准,将静态常数用作普通整数是非法的(除非该常数在。cpp 文件,作为类的任何其他静态数据成员)。但是,大多数编译器都允许这样做,只要代码不试图获取常量的地址或将它绑定到常量引用。在现代 C++ 中,这一要求被删除了。
此外,该语言允许声明一个静态整数常量(在函数范围内,而不是在类范围内),该常量由动态初始化,因此不是编译时常量:
static const int x = INT_MAX; // static
static const int y = std::numeric_limits<int>::max(); // dynamic
static const int z = rand(); // dynamic
double data[y]; // error
实际上,一个枚举通常相当于一个小整数。除非枚举的值太大,否则它们通常被实现为带符号的 int。最重要的区别是,如果没有显式强制转换,就不能将未命名的枚举绑定到模板参数:
double data[10];
std::fill_n(data, is_prime<3>::value, 3.14); // may give error!
前面的代码是不可移植的,因为可能定义了 std::fill_n。
template <..., typename integer_t, ...>
void fill_n(..., integer_t I, ...)
{
++I; // whatever...
--I; // whatever...
}
error C2675: unary '--' : "does not define this operator or a conversion to a type acceptable to the predefined operator
see reference to function template instantiation
'void std::_Fill_n<double*,_Diff,_Ty>(_OutIt,_Diff,const _Ty &,std::_Range_checked_iterator_tag)' being compiled
with
[
_Diff=,
_Ty=double *,
_OutIt=double **
]
实际上,枚举可以存储小整数(例如,以 2 为底的整数的对数)。因为它的类型不是显式的,所以在处理潜在的大型或无符号常量时应该避免使用它。作为 std::fill_n 调用的变通方法,只需将枚举转换为适当的整数:
std::fill_n(..., int(is_prime<3>::value), ...); // now ok!
通常,元函数会调用助手类(稍后您将会看到更多的例子):
template <int N>
struct ttnp1_helper
{
static const int value = (1<<N);
};
template <int N>
struct two_to_plus_one
{
static const int value = ttnp1_helper<N>::value + 1;
};
辅助变量的道德等价物是私有成员。从 TMP 的角度来看,数值常量和类型(def)是等效的编译时实体。
template <int N>
struct two_to_plus_one
{
private:
static const int aux = (1<<N);
public:
static const int value = aux + 1;
};
helper 类不私有不隐藏, 36 但是不应该使用,所以名字用 _helper 或者 _t(或者两者都用)来“丑化”。
1.3.7.名称空间和使用声明
通常,所有“公共”框架对象都被分组到一个公共的名称空间中,而“私有”对象则位于特殊的嵌套名称空间中。
namespace framework
{
namespace undocumented_private
{
void handle_with_care()
{
// ...
};
}
inline void public_documented_function()
{
undocumented_private::handle_with_care();
}
}
不必要地增加名称空间的数量不是一个好主意,因为依赖于参数的名称查找可能会引入微妙的问题,并且不同名称空间中的对象之间的友元声明是有问题的,甚至是不可能的。
通常,通用元编程框架的核心是一组头文件(扩展名为*。hpp 实际上用于纯 C++ 头文件)。在头文件中使用命名空间声明 通常被认为是不好的做法:
my_framework.hpp
using namespace std;
main.cpp
#include "my_framework.hpp"
// main.cpp doesn't know, but it's now using namespace std
然而,头文件中的 using-function 声明通常是可以的,甚至是可取的(参见本段后面的 do_something 示例)。
using-namespace 声明的一个特殊用途是头文件版本控制。 37
这是一个非常简短的例子:
namespace X
{
namespace version_1_0
{
void func1();
void func2();
}
namespace version_2_0
{
void func1();
void func2();
}
#ifdef USE_1_0
using namespace version_1_0;
#else
using namespace version_2_0;
#endif
}
因此,使用头部的客户端总是引用 X::func1。
现在我们将详细描述另一种情况,在这种情况下使用声明会有所不同。
函数模板通常用于提供“外部接口”,它是一组全局函数,允许算法执行对象的一般操作 38 :
一个虚构框架 1 的作者提供了一个函数 is_empty,它作用于一大类容器和 C 字符串:
// framework1.hpp
MXT_NAMESPACE_BEGIN(framework1)
template <typename T>
inline bool is_empty(T const& x)
{
return x.empty(); // line #1
}
template <>
inline bool is_empty(const char* const& x)
{
return x==0 || *x==0;
}
MXT_NAMESPACE_END(framework1)
这种方法的一个优点是易于扩展。对于任何新的 X 类型,您可以提供一个专用的 is_empty,它将优先于默认实现。但是,考虑一下如果函数被显式限定会发生什么:
// framework2.hpp
#include "framework1.hpp"
MXT_NAMESPACE_BEGIN(framework2)
template <typename string_t>
void do_something(string_t const& x)
{
if (!framework1::is_empty(x)) // line #2
{
// ...
}
}
MXT_NAMESPACE_END(framework2)
#include "framework2.hpp"
namespace framework3
{
class EmptyString
{
};
bool is_empty(const EmptyString& x)
{
return true;
}
}
int main()
{
framework3::EmptyString s;
framework2::do_something(s); // compiler error in line #1
}
第 2 行中用户提供的 is_empty 被忽略,因为 do_something 显式地从名称空间 framework1 中获取 is_empty。要解决这个问题,您可以重新打开名称空间 framework1 并在那里专门化 is_empty,或者修改 do _ 如下所示:
framework2.hpp
MXT_NAMESPACE_BEGIN(framework2)
using framework1::is_empty;
template <typename string_t>
void do_something(string_t const& x)
{
if (!is_empty(x))
{
//...
}
};
因此,您让参数相关的查找选择一个可用的 is_empty,但确保 framework1 可以始终提供一个默认的候选项(也参见 1.4.2 节中的讨论)。
1.4.经典图案
编写框架/库时,通常会使用和重用一小组名称。例如,容器应该有一个成员函数[[integer type]] size() const,它返回元素的数量。
采用统一的风格增加了对象的互操作性;更多详情,请参见第 6 章。以下所有段落将尝试描述与几个常见 C++ 名称相关的传统含义。
1.4.1.size_t 和 ptrdiff_t
在 C++ 中,没有唯一的标准和可移植的方法来命名大整数。现代编译器通常会为长整型和无符号长整型选择最大的整数。当你快速需要一个大而快的整数时,首选是 size_t (无符号)和 ptrdiff_t (有符号)。
size_t 是 sizeof 和运算符 new 的结果,它足够大,可以存储任意数量的内存;ptrdiff_t 表示两个指针的差。因为字符数组的长度是首尾相连的,所以根据经验,它们的大小是一样的。
此外,在平面 C++ 内存模型中,sizeof(size_t)也将是指针的大小,这些整数可能具有体系结构中的自然大小——比方说,在 32 位处理器上是 32 位,在 64 位处理器上是 64 位。它们也很快(处理器总线将执行从寄存器到存储器的原子传输)。
给定这个类:
template <int N>
struct A
{
char data[N];
};
sizeof(A )至少是 N,那么也可以得出 size_t 不小于 int。 39
1.4.2.void T::swap(T&)
这个函数应该在常数时间内交换this 和参数,而不抛出异常。常数的实际定义是“仅依赖于 T 的时间量”。* 40
如果 T 有一个交换成员函数,用户期望它不会比传统的三份交换差(即 X = A;a = B;B=X)。事实上,这总是可能的,因为成员函数可以调用每个成员自己的交换:
class TheClass
{
std::vector<double> theVector_;
std::string theString_;
double theDouble_;
public:
void swap(TheClass& that);
{
theString_.swap(that.theString_);
theVector_.swap(that.theVector_);
std::swap(theDouble_, that.theDouble_);
}
};
唯一需要不固定时间的步骤是逐个元素地交换动态数组,但这可以通过整体交换数组来避免。
类 std::tr1::array 有一个 swap,在一个长度为 N 的数组上调用 std::swap_range,这样占用的时间与 N 成正比,依赖于 t,但是 N 是类型的一部分,所以根据这个定义,它是常数时间。此外,如果 T 是可交换类型(例如 std::string),swap_range 的性能将比三次复制过程好得多,因此成员交换肯定是一个优势。
要解决的第一个问题是如何交换未指定类型的对象:
template <typename T>
class TheClass
{
T theObj_; // how do you swap two objects of type T?
void swap(TheClass<T>& that)
{
std::swap(theObj_, that.theObj_);
}
};
显式限定 std::是一个不必要的约束。你最好引入一个 using 声明,见 1.3.7 节:
using std::swap;
template <typename T>
class TheClass
{
T theObj_;
public:
void swap(TheClass<T>& that) // line #1
{
swap(theObj_, that.theObj_); // line #2
}
};
但是,这会导致编译器错误,因为根据通常的 C++ 名称解析规则,第 2 行中的 swap 是第 1 行中定义的 swap,它没有两个参数。
解决方案是引入一个名字不同的全局函数,这个习惯用法叫做与 ADL 互换:
using std::swap;
template <typename T>
inline void swap_with_ADL(T& a, T& b)
{
swap(a, b);
}
template <typename T>
class TheClass
{
T theObj_;
public:
void swap(TheClass<T>& that)
{
swap_with_ADL(theObj_, that.theObj_);
}
根据查找规则,swap_with_ADL 将调用转发到与 T 在同一个名称空间中定义的交换函数(希望是 T 自己的版本),或者转发到 std::swap(如果不存在其他函数)。由于没有同名的局部成员函数,所以 lookup 会跳过类级别。
互换的传统说法是 T & amp;但是,提供更多的重载可能是有意义的。如果一个对象在内部将其数据保存在 X 类型的标准容器中,那么提供 void swap(X&)可能是有用的,它具有宽松的时间复杂度预期:
template <typename T>
class sorted_vector
{
std::vector<T> data_;
public:
void swap(sorted_vector<T>& that)
{
data_.swap(that.data_);
}
void swap(std::vector<T>& that)
{
data_.swap(that);
std::sort(data_.begin(), data_.end());
}
};
更有 41 :
struct unchecked_type_t {};
inline unchecked_type_t unchecked() { return unchecked_type_t(); }
template <typename T>
class sorted_vector
{
// ...
void swap(std::vector<T>& that, unchecked_type_t (*)())
{
assert(is_sorted(that.begin(), that.end()));
data_.swap(that);
}
};
sorted_vector<double> x;
std::vector<double> t;
load_numbers_into(x);
x.swap(t);
// now x is empty and t is sorted
// later...
x.swap(t, unchecked); // very fast
总而言之:
- 用固定本机类型(整数、指针等)和标准容器(包括字符串)的参数显式限定 std::swap。
- 为 std::swap 编写 using 声明,当参数在全局函数中有未定义的类型 T 时调用非限定交换。
- 在具有交换成员函数的类中调用 swap_with_ADL。
std::swap 提供了交换本机类型和 std 类型的最佳实现。
交换用于具有移动语义的算法中:
void doSomething(X& result)
{
X temp;
// perform some operation on temp, then...
swap(temp, result);
}
以及根据复制构造函数实现异常安全赋值运算符:
class X
{
public:
X(const X&);
void swap(X&);
~X();
X& operator=(const X& that)
{
X temp(that); // if an exception occurs here, *this is unchanged
temp.swap(*this); // no exception can occur here
return *this; // now temp is destroyed and releases resources
}
};
如果执行无条件交换,最有效的解决方案是按值获取参数:
X& operator=(X that)
{
that.swap(*this);
return *this;
}
另一方面,您可能希望在手工调用复制构造函数之前执行额外的检查,即使这样效率更低 42 :
X& operator=(const X& that)
{
if (this != &that)
{
X temp(that);
temp.swap(*this);
}
return *this;
}
缺点是在某些时候,that 和 temp 都是活动的,所以您可能需要更多的空闲资源(例如,更多的内存)。
1.4.3.bool T::empty()const;void T::clear()
前一个函数测试一个对象是否为空;后者使之空虚。如果一个对象有一个成员函数 size(),那么调用 empty()应该不会比 size()==0 慢。
请注意,对象可能是空的,但仍然控制资源。例如,一个空的 vector 可能包含一个原始的内存块,实际上还没有构造任何元素。
特别是,它没有指明一个 clear 函数是否会释放对象资源;clear 是 reset 的同义词。
要对 auto 变量强制进行资源清理,通常的方法是用一个临时的:
T x;
// now x holds some resources...
T().swap(x);
1.4.4.x T::get()const;x T::base()constT3】
当类型 T 包装一个更简单的类型 x 时,使用 get 这个名称。因此,智能指针的 get 将返回内部普通指针。
相反,当包装器只是一个不同的接口时,函数库用于返回被包装对象的副本。因为智能指针通常会增加一些复杂性(例如,引用计数),所以命名基不像 get 那样合适。另一方面,std::reverse_iterator 是一个交换底层迭代器的++ 和-的接口,所以它有一个 base()。
1.4.5.x T::property()constT1】;void T::property(X)T3】
在本节中,“属性”是一个符号名称。一个类可以公开两个名为“property”的重载成员函数,它们有两种不同的意图。
第一种形式返回当前实例的属性的当前值;第二个将属性设置为某个新值。属性集函数也可以具有以下形式:
X T::property(X newval)
{
const X oldval = property();
set_new_val(newval);
return oldval;
}
这种约定很优雅,但并不普遍使用;它存在于 std::iostream 中。
1.4.6.动作(值);动作(范围)
在本节中,“action”也是重载函数或成员函数的符号名。
如果对象自身的操作(例如 container.insert(value))可能会被顺序调用,则对象可能会提供一个或多个范围等效项。换句话说,它可以为成员函数提供两个或多个参数,一次标识一系列元素。一些常见的例子有:
- 一个元素和一个重复计数器
- 两个迭代器指向(begin...结束)
- 一个数组和两个索引
利用预先知道的范围取决于实现。像往常一样,值域等价函数永远不应该比琐碎的实现动作(range):= for(x in range){ action(x);}.
1.4.7.机械手
操纵器是 C++ 标准中最不为人知和最富表现力的部分之一。它们只是将流作为参数的函数。因为它们的签名是固定的,所以流有一个特殊的插入操作符来运行它们:
class ostream
{
public:
ostream& operator<<(ostream& (*F)(ostream&))
{
return F(*this);
}
inline ostream& endl(ostream& os)
{
os << '\n';
return os.flush();
}
};
int main()
{
// actually execute endl(cout << "Hello world")
std::cout << "Hello world" << std::endl;
}
一些操纵者有一个论点。实现可以使用模板代理对象将该参数传输到流:
struct precision_proxy_t
{
int prec;
};
inline ostream& operator<<(ostream& o, precision_proxy_t p)
{
o.precision(p.prec);
return o;
}
precision_proxy_t setprecision(int p)
{
precision_proxy_t result = { p };
return result;
}
cout << setprecision(12) << 3.14;
注意,更现实的实现可能想要在代理中嵌入函数指针,以便只有一个插入操作符:
class ostream;
template <typename T, ostream& (*FUNC)(ostream&, T)>
struct proxy
{
T arg;
proxy(const T& a)
: arg(a)
{
}
};
class ostream
{
public:
template <typename T, ostream& (*FUNC)(ostream&, T)>
ostream& operator<<(proxy<T, FUNC> p)
{
return FUNC(*this, p.arg);
}
};
ostream& global_setpr(ostream& o, int prec)
{
o.precision(prec);
return o;
}
proxy<int, global_setpr> setprecision(int p)
{
return p;
}
cout << setprecision(12) << 3.14;
template <typename T>
struct proxy
{
T arg;
ostream& (*FUNC)(ostream&, T);
};
class ostream
{
public:
template <typename T>
ostream& operator<<(proxy<T> p)
{
return p.FUNC(*this, p.arg);
}
};
原则上,函数模板可以用作操纵器,例如:
stream << manip1;
stream << manip2(argument);
stream << manip3<N>;
stream << manip4<N>(argument);
但实际上这是不鼓励的,因为许多编译器不接受 manip3。
1.4.8.操作员的位置
理解成员和非成员操作符之间的区别很重要。
当成员操作符被调用时,左边已经被静态地确定了,所以如果需要任何调整,只在右边执行。或者,非成员操作符将只精确匹配或给出错误。
假设您正在重写 std::pair:
template <typename T1, typename T2>
struct pair
{
T1 first;
T2 second;
template <typename S1, typename S2>
pair(const pair<S1, S2>& that)
: first(that.first), second(that.second)
{
}
};
现在加上运算符==。首先作为成员:
template <typename T1, typename T2>
struct pair
{
// ...
inline bool operator== (const pair<T1,T2>& that) const
{
return (first == that.first) && (second == that.second);
}
};
然后编译以下代码:
pair<int, std::string> P(1,"abcdefghijklmnop");
pair<const int, std::string> Q(1,"qrstuvwxyz");
if (P == Q)
{ ... }
这将起作用,并将调用 pair ::operator==。这个函数需要一个对对的常量引用,但它被给了对。它将静默地调用模板复制构造函数,并复制右边的对象,这是不可取的,因为它将生成字符串的临时副本。
最好将操作符放在类之外:
template <typename T1, typename T2>
bool operator== (const pair<T1,T2>& x, const pair<T1,T2>& y)
{
return (x.first == y.first) && (x.second == y.second);
}
至少,这段代码现在将无法编译,因为等式现在需要相同的对。明显的失败总是比微妙的问题更可取。
类似于经典的 C++ 规则,“如果你写一个定制的复制构造函数,那么你将需要一个定制的赋值操作符,”我们可以说,如果你写一个通用的复制构造函数,你将可能需要通用操作符,以避免临时转换的开销。在这种情况下,要么使用带有两个参数的模板成员函数,要么使用带有四个参数的全局运算符。有些程序员更喜欢全局操作符,如果可以只使用类的公共接口来实现它们的话(如前所示)。
template <typename T1, typename T2 >
struct pair
{
// ...
template <typename S1, typename S2>
inline bool operator== (const pair<S1, S2>& that) const
{
return (first == that.first) && (second == that.second);
}
};
如果 this->first 和 that.first 是可比较的(例如,int 和 const int),这将起作用。请注意,您可能仍然有临时转换,因为您正在委托给未指定的 T1::operator==。 43
1.4.9.秘密继承
从具体类的公共派生可以用作一种“强 typedef”:
class A
{
// concrete class
// ...
};
class B : public A
{
};
// now B works "almost" as A, but it's a different type
你可能需要在 b 中实现一个或多个“转发构造函数”。
这是模拟模板 typedefs 的策略之一(这在 C++ 中还不存在;参见第 12.6 节):
template <typename T1, typename T2>
class A
{
// ...
};
template <typename T>
class B : public A<T, T>
{
};
但是,只有当是一个存在未知或未记录的私有类时,这才是可接受的:
template <typename T>
class B : public std::map<T, T> // bad idea
namespace std
{
template <...>
class map : public _Tree<...> // ok: class _Tree is invisible to the user
秘密基类通常是不依赖于某些模板参数的操作符的良好容器。例如,测试两个对象之间的相等性可能是合理的,忽略所有纯粹装饰性的参数:
template <typename T, int INITIAL_CAPACITY = 16>
class C;
template <typename T>
class H
{
public:
H& operator==(const H&) const;
};
template <typename T, int INITIAL_CAPACITY>
class C : public H<T>
{
};
具有不同 INITIAL_CAPACITY 的两个容器 C 之间的比较将会成功,并调用它们的公共库 H::operator==。
1.4.10.文字零
有时候你需要写一个函数或者一个操作符,当一个文字零被传递时,它的行为会有所不同。智能指针经常出现这种情况:
template <typename T>
class shared_ptr
{
//...
};
shared_ptr<T> P;
T* Q;
P == 7; // should not compile
P == 0; // should compile
P == Q; // should compile
您可以通过编写一个重载来区分 0 和泛型 int,该重载接受指向没有成员的类的成员的指针:
class dummy {};
typedef int dummy::*literal_zero_t;
template <typename T>
class shared_ptr
{
// ...
bool operator==(literal_zero_t) const
{
用户无法创建 literal_zero_t,因为 dummy 没有 int 类型的成员,所以唯一有效的参数是 literal zero 的隐式强制转换(除非存在更专用的重载)。
1.4.11.布尔类型
有些类型,比如 std::stream,有一个 cast-to-boolean 操作符。如果天真地实施,这可能会导致不一致:
class stream
{
// ...
operator bool() const
{
// ...
}
};
stream s;
if (s) // ok, that's what we want
{
int i = s + 2; // unfortunately, this compiles
}
一个经典的解决方法是实现强制转换为 void*:
class stream
{
// ...
operator void*() const
{
// return 'this' when true or '0' when false
}
};
stream s;
if (s) // ok, that's what we want
{
int i = s + 2; // good, this does not compile...
free(s); // ...but this goes on
}
更好的解决方案还是指向成员的指针:
struct boolean_type_t
{
int true_;
};
typedef int boolean_type_t::*boolean_type;
#define mxt_boolean_true &boolean_type_t::true_
#define mxt_boolean_false 0
class stream
{
// ...
operator boolean_type() const
{
// return mxt_boolean_true or mxt_boolean_false
}
1.4.12.默认值和值初始化
如果 T 是一个类型,那么默认实例的构造并不意味着对象本身的初始化。的确切效果
T x;
严重依赖于 T。如果 T 是基本类型或 POD,则它的初始值是未定义的。如果 T 是一个类,它的一些成员可能还没有定义:
class A
{
std::string s_;
int i_;
public:
A() {} // this will default-construct s_ but leave i_ uninitialized
};
另一方面,这条线
T x = T();
会将 T 初始化为 0,比方说对于所有的基本类型,但是如果 T 是 A,它可能会崩溃,因为将未初始化的成员 i_ 从右边的 temporary 复制到 x 是非法的。
所以总结一下:
T a(); // error:
// a is a function taking no argument and returning T
// equivalent to T (*a)()
T b; // ok only if T is a class with default constructor
// otherwise T is uninitialized
T c(T()); // error: c is a function taking a function and returning T
// equivalent to T (*c)(T (*)())
T d = {}; // ok only if T is a simple aggregate<sup class="calibre7">44</sup> (e.g. a struct
// without user-defined constructors)
T e = T(); // requires a non-explicit copy constructor
// and may yield undefined behaviour at runtime
值初始化(见标准的第 8.5.1-7 段)是解决这个问题的一种方法。因为它只对类成员有效,所以您必须编写:
template <typename T>
struct initialized_value
{
T result;
initialized_value()
: result()
{
}
};
如果 T 是一个有默认构造函数的类,那么将使用它;否则,T 的存储将被设置为 0。如果 T 是一个数组,每个元素将被递归初始化:
initialized_value<double> x; // x.result is 0.0
initialized_value<double [5]> y; // y.result is {0.0, ..., 0.0}
initialized_value<std::string> z; // z.result is std::string()
1.5.代码安全
TMP 的精神是“优雅第一”。理论上,一些技术可以打开源代码中的漏洞,恶意程序员可以利用这些漏洞使程序崩溃。 45
考虑以下情况:
#include <functional>
class unary_F : public std::unary_function<int,float>
{
public:
// ...
};
int main()
{
unary_F u;
std::unary_function<int,float>* ptr = &u; // ok, legal!
delete ptr; // undefined behaviour!
return 0;
}
系统头文件可以通过在一元函数中定义一个受保护的析构函数来使反例失败:
template<class _Arg, class _Result>
struct unary_function
{
typedef _Arg argument_type;
typedef _Result result_type;
protected:
~unary_function()
{
}
};
但是这种情况一般不会发生。 46
以下想法是由于萨特(【4】):
myclass.h
class MyClass
{
private:
double x_;
int z_;
public:
template <typename stream_t>
void write_x_to(stream_t& y)
{
y << x_;
}
};
是否可以合法读取/修改私有成员 MyClass::z_?只需在包含 myclass.h 之后的某个地方添加一个专门化即可:
struct MyClassHACK
{
};
template <>
void MyClass::write_x_to(MyClassHACK&)
{
// as a member of MyClass, you can do anything...
z_ = 3;
}
最后,声明模板友谊时也有问题。首先,没有标准和可移植的方法来用模板参数声明友谊(更多细节请参考**【5】**)。
template <typename T, int N>
class test
{
friend class T; // uhm...
};
第二,没有办法让 test 成为 test 的朋友(没有什么比部分模板友谊更好的了)。一个常见的解决方法是将测试声明为任何其他类型 x 的测试的朋友
template <typename T, int N>
class test
{
template <typename X, int J>
friend class test; // ok, but every test<X,J> has access
};
编写 MyClassHACK 的同一个恶意用户可以添加:
template <>
class test<MyClassHACK, 0>
{
public:
template <typename T, int N>
void manipulate(test<T,N>& x)
{
// a friend can do anything!
}
};
您将会看到,TMP 有时会利用在传统 C++ 中被正确标记为不良实践的技术,包括(但不限于):
- (空)基类中缺少非虚拟的受保护析构函数
- 实现强制转换运算符运算符 T() const
- 用单个参数声明非显式构造函数
1.6.编译器假设
大量使用模板意味着编译器的大量工作。并非所有符合标准的技术在每一个平台上都表现相同。 47
你用与语言无关的习语 来表示没有标准规定的行为,而只有合理的预期行为的所有语言特征。换句话说,当你使用与语言无关的习惯用法时,你可以预期大多数编译器会收敛于某些(最优)行为,即使标准没有要求它们这样做。
注意例如,C++ 标准规定,对于任何类型 T,sizeof(T) > 0,但不要求复合类型的大小最小。一个空结构的大小可以是 64,但是我们期望它的大小是 1(或者在最坏的情况下,大小不大于一个指针)。
符合标准的编译器可以合法地违反最优性条件,但是在实践中,这种情况很少发生。换句话说,语言中立的习惯用法是一种语言结构,它不会使程序变得更糟,但会给优秀的编译器提供一个很好的优化机会。
一个完全符合标准的代码片段可能会产生几个问题:
- 意外的编译器错误
- 运行时故障(访问违规、核心转储、蓝屏和恐慌反应)
- 巨大的编译/链接时间
- 次优运行速度
前两个问题是由于编译器错误造成的,并且涉及到寻找语言变通方法(但是第二个问题通常在为时已晚的时候遇到)。
第三个问题主要依赖于糟糕的模板代码。
第四个问题涉及到寻找优化器不能识别的语言无关的习惯用法,因此不必要地降低了程序的执行速度。
我们关心的预期行为的一个例子是向基类添加一个空析构函数。
class base
{
public:
void do_something() {}
protected:
~base() {}
};
class derived : public base
{
};
因为空析构函数不添加任何代码,所以我们希望有和没有它的可执行文件都是相同的。 48
我们假设编译器能够理解并以最佳方式处理以下段落中列出的情况。
1.6.1.内嵌
编译器必须能够自己管理函数内联,忽略内联指令和代码定位(成员函数体在此编写)。
全内联样式将定义和声明放在类体中;每个成员函数都是隐式内联的:
template <typename T>
class vector
{
public:
bool empty() const
{
// definition and declaration
}
};
合并的头样式将非内联成员函数的定义和声明分开,但是将它们保存在同一个文件中:
template <typename T>
class vector
{
public:
bool empty() const; // declaration, non inline
};
template <typename T>
bool vector <T>::empty() const
{
// definition
}
在任何情况下,无论您是否显式地编写它,内联指令都不仅仅是一个提示。一些流行的编译器确实可以根据编译器的判断选择内联任何函数。
具体来说,我们假设
-
如果函数足够简单,无论序列有多长,内联函数序列总是“最优的”:
template <typename T, int N> class recursive { recursive<T,N-1> r_; public: int size() const { return 1 + r_.size(); } }; template <typename T> class recursive<T, 0> { public: int size() const { return 0; } };
在前面的构造中,递归:::size()将被内联,优化器将简化调用以返回 N. 49
- 编译器可以优化对无状态对象的(const)成员函数的调用,典型的例子是二元关系的运算符()。
让一个类持有一个仿函数的副本作为私有成员是一种常见的 STL 习惯用法:
template <typename T>
struct less
{
bool operator()(const T& x, const T& y) const
{
return x<y;
}
};
template < typename T, typename less_t = std::less<T> >
class set
{
less_t less_; // the less functor is a member
public:
set(const less_t& less = less_t())
: less_(less)
{
}
void insert(const T& x)
{
// ...
if (less_(x,y)) // invoking less_t::operator()
// ...
}
};
如果函子确实是无状态的,并且 operator()是 const,前面的代码应该相当于:
template <typename T>
struct less
{
static bool apply(const T& x, const T& y)
{
return x<y;
}
};
template < typename T, typename less_t = std::less<T> >
class set
{
public:
void insert(const T& x)
{
// ...
if (less_t::apply(x,y))
{}
}
};
然而,您为更大的通用性付出了代价,因为 less_ member 将消耗至少一个字节的空间。如果编译器实现了 EBO ( 空基优化),就可以解决这两个问题。
class stateless_base
{
};
class derived : public stateless_base
{
// ...
};
换句话说,从无状态基类的任何派生都不会使派生类更大。 50 如果 less 实际上是一个无状态结构,EBO 就不会给 set 的布局增加额外的字节。
template <typename T>
struct less
{
bool operator()(const T& x, const T& y) const
{
return x<y;
}
};
template < typename T, typename less_t = std::less<T> >
class set : private less_t
{
inline bool less(const T& x, const T& y) const
{
return static_cast<const less_t&>(*this)(x,y);
}
public:
set(const less_t& l = less_t())
: less_t(l)
{
}
void insert(const T& x)
{
// ...
if (less(x,y)) // invoking less_t::operator() through *this
{}
}
};
请注意辅助成员函数 less,它旨在防止与任何其他 set::operator()发生冲突。
1.6.2.错误消息
您希望编译器给出精确而有用的错误诊断,尤其是在处理模板时。不幸的是,“精确”和“有用”的含义对于人类和编译器来说可能不一样。
有时,TMP 技术特别诱导编译器在错误信息中输出提示。另一方面,用户应该准备好从编译器日志中包含的一些关键字中找出确切的错误,忽略所有的干扰。这里有一个噪音的例子:
\include\algorithm(21) : error 'void DivideBy10<T>::operator ()(T &) const' : cannot convert parameter 1 from 'const int' to 'int &'
with
[
T=int
]
Conversion loses qualifiers
iterator.cpp(41) : see reference to function template instantiation '_Fn1
std::for_each<XT::pair_iterator<iterator_t,N>,DivideBy10<T>>(_InIt,_InIt,_Fn1)'
being compiled
with
[
_Fn1= DivideBy10<int>,
iterator_t=std::_Tree<std::_Tmap_traits<int,double,std::less<int>,std::allocator
<std::pair<const int,double>>,false>>::iterator,
N=1,
T=int,
_InIt=XT::pair_iterator<std::_Tree<std::_Tmap_traits<int,double,std::less<int>,
std::allocator<std::pair<const int,double>>,false>>::iterator,1>
]
下面是用户应该看到的内容:
iterator.cpp(41) : error in 'std::for_each (iterator, iterator, DivideBy10<int>)'
with
iterator = XT::pair_iterator<std::map<int, double>::const_iterator, 1>
'void DivideBy10<T>::operator ()(T &) const' : cannot convert parameter 1 from 'const int' to 'int &'
这意味着 for_each 的调用者想要改变(也许除以 10?)std::map 的(常量)键,这是非法的。而最初的错误指向
,真正的问题在 iterator.cpp。错误消息中出现不友好的条目是因为编译器看到的“基本错误”可能与语义错误“相距甚远”。
长模板堆栈
如前所示,函数模板可以报告一个错误,这是由于从其调用方传递的参数。现代编译器会列出整个模板实例链。由于函数模板通常依赖于模板框架,这些错误通常在函数调用堆栈中的几个层次上。
实施细节
在前面的例子中,编译器显示的是 std::_Tree 而不是 std::map,因为 map::iterator 恰好是在单独的基类(named _Tree)中定义的。std::map 有一个公共 typedef,它从基类中借用了一个迭代器:
typedef typename _Tree<...>::iterator iterator;
这些通常对 std::map 用户隐藏的实现细节可能会在错误日志中泄漏。
扩展的 Typedefs
std::string 的错误可能显示为 std::basic_string <char ...="">,因为一些编译器会用它们的定义替换 typedefs。替换可能会引入用户未知的类型。
然而,编译器真的不可能决定执行这些替换是否方便。
假设有两个元函数叫做 F:::type 和 G:::type:
typedef typename G<T>::type GT;
typedef typename F<GT>::type FGT;
可能会出现错误
-
当 T 不是 G 的有效参数时,在这种情况下你想读:
error "F<GT> [where GT=G<int>::type]...".
-
因为 G:::type(已定义但用户未知)被 F 拒绝,所以可能更有用:
error "F<GT> [where GT=double]...". ```</t>
然而,如果你不知道 G 的结果,一个日志条目比如 F[其中 X=double]...可能会产生误导(您可能甚至没有意识到您正在调用 F )。
不完整的类型
如果使用得当,不完整的类型会导致特定的错误(参见 2.2 节)。然而,有些情况下,一个类型还没有完成,这可能会导致奇怪的错误。附录 a 中有一个很长的有启发性的例子。
通常,当编译器说“常量不是常量”或“类型不是类型”时,这通常意味着你要么递归地定义一个常量,要么使用一个尚未完成的类模板。
1.6.3.杂项提示
不管假设如何,真正的编译器可以做任何事情,所以本节概述了一些通用技巧。
不要责怪编译器
虫子会撒谎:
- 在代码中,以概率(100-ε)%
- 在优化器中,概率略大于(ε/2)%
- 在编译器中,概率小于(ε/2)%
即使只在发布版本中出现的问题也很少是由优化器错误引起的。调试版本和发布版本之间有一些自然的差异,这可能会隐藏程序中的一些错误。常见的因素有#ifdef 节、未初始化的变量、调试分配器返回的零填充堆内存等等。
编译器确实有 bug,但是一个常见的误解是它们只在发布版本中出现。由 MSVC7.1 编译的以下代码在发布时产生正确的值,而在调试时不产生正确的值:
#include <iostream>
int main()
{
unsigned __int64 x = 47;
int y = -1;
bool test1 = (x+y)<0;
x += y;
bool test2 = (x<0);
bool test3 = (x<0);
std::cout << test1 << test2 << test3; // it should print 000
return 0;
}
调试版本中 Mac OSX 的 GCC4 不会警告用户在一个控制台程序中有多个主函数,它会自动生成一个什么也不做的可执行文件。 51
将警告保持在默认级别
警告只是猜测。所有的编译器都能够识别“习惯用法”,这些习惯用法很可能是人为错误的征兆。概率越高,警告级别越低。显示顶级警告不太可能揭示错误,但是它会用无害的消息淹没编译器日志。 52
不要用“脏”代码修改来消除警告
如果某个特定的警告是令人讨厌的、合法的,并且可能不是错误,那么就不要修改代码。将编译器特定的#pragma disable-warning 指令放在该行的周围。这对未来的代码审查者很有用。
但是,应该小心使用这种解决方案(深度嵌套的函数模板中的警告可能会在编译器日志中生成许多冗长的虚假条目)。
应该而不是修复的最危险的警告之一是“有符号/无符号比较”。
混合操作数之间的许多二元运算都涉及到将两者提升为无符号运算,负数会变成正数,而且非常大。编译器会在某些情况下发出警告,而不是全部。
bool f(int a)
{
unsigned int c = 10;
return ((a+5)<c);
}
test01.cpp(4) : warning C4018: '<' : signed/unsigned mismatch
对于∈ {-5,-4,该函数返回 true,...,4}.如果将 c 改为 int,警告会消失,但是函数的行为会有所不同。
元函数中的相同代码不会产生任何警告:
template <int A>
class BizarreMF
{
static const int B = 5;
static const unsigned int C = 10;
public:
static const bool value = ((A+B)<C);
};
bool t = BizarreMF<-10>::value; // returns false
在实际代码中,有两种情况可能容易出现“符号性错误”:
-
将元函数返回类型从枚举更新为静态无符号常量:
static const bool value = (A+5) < OtherMF<B>::value; // unpredictable result: the type of OtherMF is unknown / may vary
-
Changing a container:
C++ 标准没有明确定义数组索引的整数类型。如果 p 有类型 T*,那么 p[i] == *(p+i),那么我应该有类型 ptrdiff_t,它是有符号的。然而,vector ::operator[]采用无符号索引。
总而言之,警告是:
- 编译器特定的
- 与代码正确性无关(既有产生警告的正确代码,也有编译干净的错误代码)
编写尽可能不产生警告的代码。
维护一个编译器错误目录
这在升级编译器时非常有用。
避免不规范的行为
这个建议在每一本关于 C++ 的书中都有,但是我们在这里重复一遍。程序员 54 倾向于用自己喜欢的编译器作为主要工具来判定一个程序是否正确,而不是 C++ 标准。一个合理的经验标准是使用两个或更多的编译器,如果他们不同意,检查标准。
不要害怕语言特性
只要有原生 C++ 关键字、函数或 std:: object,就可以认为不可能做得更好,除非牺牲一些特性。 55
C++ 程序中的严重瓶颈通常与语言特性的误用有关(有些特性比其他特性更容易被误用;候选项是虚函数和动态内存分配),但这并不意味着应该避免这些特性。
任何操作系统都可以足够快地分配堆内存,以至于合理数量的对 operator new 的调用都不会被注意到。 56
一些编译器允许你通过一个名为 alloca 的函数从堆栈中获取一点内存;原则上,alloca 后跟一个 placement new(和一个显式的析构函数调用)大致相当于 new,但是它会导致对齐问题。虽然标准允许堆内存适合任何类型,但这不适用于堆栈。更糟糕的是,在未对齐的内存上构建对象可能会在某些平台上偶然工作,而且在完全未被发现的情况下,可能会降低所有数据操作的速度。 57
相反的情况是交易特性。在强有力的额外假设下,有时可能做得比新的更好;例如,在分配/解除分配模式已知的单线程程序中:
// assume T1 and T2 are unspecified concrete types, not template parameters
std::multimap<T1, T2> m;
while (m.size()>1)
{
std::multimap<T1, T2>::iterator one = ...; // pick an element.
std::multimap<T1, T2>::iterator two = ...; // pick another one.
std::pair<T1, T2> new_element = merge_elements(*one, *two);
m.erase(one); // line #1
m.erase(two); // line #2
m.insert(new_element); // line #3
}
在这里,您可能希望胜过默认的基于 new 的分配器,因为两次删除之后总是跟着一次分配。粗略地说,当这由系统 new/delete 处理时,必须通知操作系统在第 2 行有更多的可用内存,但是第 3 行立即回收相同数量的内存。 58
想想你的代码的用户会怎么做
人类的记忆不如电脑记忆持久。一些在经典 C++ 中看起来显而易见或容易推导的东西在 TMP 中可能更难。
考虑一个简单的函数,例如:
size_t find_number_in_string(std::string s, int t);
您很容易猜到该函数会在第一个参数中查找第二个参数。现在考虑:
template <typename T, typename S>
size_t find_number_in_string(S s, T t);
虽然这对作者来说看起来很自然(毕竟 S 代表字符串),但我们应该考虑一些帮助记忆的技巧。
-
任何具有代码完成功能的 IDE 都将显示参数名 :
template <typename T, typename S> size_t find_number_in_string(S str, T number); template <typename NUMBER_T, typename STRING_T> size_t find_number_in_string(STRING_T str, NUMBER_T number);
-
在函数前的代码中插入一行注释;IDE 可以选择它并显示工具提示。
-
对参数的顺序或结果类型采用某种约定(比如 C 的 memcpy)。
1.7.预处理器
1.7.1.包括防护装置
如前所述,一个项目通常分布在许多源文件中。必须对每个文件进行组织,以便所有依赖项和先决条件都由包含的文件检查,而不是由调用者检查。特别是,头包含不应该依赖于#include 语句的顺序。
file "container.hpp"
#include <vector> // dependency is resolved here, not outside
#ifdef _WIN32 // preconditions are checked here
#error This file requires a 128-bit operating system. Please, upgrade.
#endif
template <typename T>
class very_large_container
{
// internally uses std::vector...
};
大多数框架最终都有一种根文件,负责准备环境:
- 当前平台的检测
- 编译器特定宏到框架宏的翻译
- 通用宏的定义(如 MXT_NAMESPACE_BEGIN)
- 包含 STL 标题
- 轻量级结构、类型定义和常量的定义
所有其他头文件都是从根文件开始的,根文件很少被修改。这通常会减少编译时间,因为可以指示编译器从根文件中提取预编译的头文件。
下面是一个例子:
///////////////////////////////////////////////////////////////////////
// platform detection
#if defined(_MSC_VER)
#define MXT_INT64 __int64
#elif defined(__GNUC__)
#define MXT_INT64 long long
#else
// ...
#endif
///////////////////////////////////////////////////////////////////////
// macro translation
// the framework will rely on MXT_DEBUG and MXT_RELEASE
#if defined(DEBUG) || defined(_DEBUG) || !defined(NDEBUG)
#define MXT_DEBUG
#else
#define MXT_RELEASE
#endif
///////////////////////////////////////////////////////////////////////
// general framework macros
#define MXT_NAMESPACE_BEGIN(x) namespace x {
#define MXT_NAMESPACE_END(x) }
///////////////////////////////////////////////////////////////////////
// STL
#include <complex>
#include <vector>
#include <map>
#include <utility>
///////////////////////////////////////////////////////////////////////
using std::swap;
using std::size_t;
typedef std::complex<double> dcmplx;
typedef unsigned int uint;
///////////////////////////////////////////////////////////////////////
struct empty
{
};
根据基本的 include guard 习惯用法,您应该将每个头包含在预处理程序指令中,这将防止在同一个翻译单元中出现多个包含:
#ifndef MXT_filename_
#define MXT_filename_
// put code here
#endif //MXT_filename_
作为这种技术的一个小变化,您可以给 MXT_filename_ 赋值。毕竟,这本书的全部意义在于将信息存储在不同寻常的地方:
#ifndef MXT_filename_
#define MXT_filename_ 0x1020 // version number
// put code here
#endif //MXT_filename_
#include "filename.hpp"
#if MXT_filename_ < 0x1010
#error You are including an old version!
#endif
无论如何,这样的保护对于包含循环是无效的。在 TMP 中循环发生得更频繁,这里只有头,没有*。cpp 文件,所以声明和定义要么一致,要么在同一个文件中。
假设 A.hpp 自成体系,B.hpp 包含 A.hpp,C.hpp 包含 B.hpp。
// file "A.hpp"
#ifndef MXT_A_
#define MXT_A_ 0x1010
template <typename T> class A {};
#endif
// file "B.hpp"
#ifndef MXT_B_
#define MXT_B _ 0x2020
#include "A.hpp"
template <typename T> class B {}; // B uses A
#endif
后来,开发人员修改了 A.hpp,使其包含 C.hpp。
// file "A.hpp"
#ifndef MXT_A_
#define MXT_A_ 0x1020
#include "C.hpp"
...
不幸的是,预处理器会生成一个文件,在 A 之前包含 B 的副本:
// MXT_A_ is not defined, enter the #ifdef
#define MXT_A_ 0x1020
// A.hpp requires including "C.hpp"
// MXT_C_ is not defined, enter the #ifdef
#define MXT_C_ 0x3030
// C.hpp requires including "B.hpp"
// MXT_B_ is not defined, enter the #ifdef
#define MXT_B _ 0x2020
// B.hpp requires including A.hpp
// however MXT_A_ is already defined, so do nothing!
template <typename T> class B {};
// end of include "B.hpp"
template <typename T> class C {};
// end of include "C.hpp"
template <typename T> class A {};
这通常会给出奇怪的错误信息。
总之,您应该检测到循环包含问题,即文件在完全编译之前(间接地)包含了自身的副本。
下面的框架头有所帮助(缩进仅用于说明目的)。
#ifndef MXT_filename_
#define MXT_filename_ 0x0000 // first, set version to "null"
#include "other_header.hpp"
/////////////////////////////////////////////////////////////
MXT_NAMESPACE_BEGIN(framework)
/////////////////////////////////////////////////////////////
// write code here
/////////////////////////////////////////////////////////////
MXT_NAMESPACE_END(framework)
/////////////////////////////////////////////////////////////
// finished! remove the null guard
#undef MXT_filename_
// define actual version number and quit
#define MXT_filename_ 0x1000
#else // if guard is defined...
#if MXT_filename_ == 0x0000 // ...but version is null
#error Circular Inclusion // ...then something is wrong!
#endif
#endif //MXT_filename_
这样的头文件不会解决循环包含(这是一个设计问题),但是编译器会尽快诊断它。无论如何,有时用一些前向声明替换#error 语句就足够了:
#ifndef MXT_my_vector_
#define MXT_my_vector_ 0x0000
template <typename T>
class my_vector
{
public:
// ...
};
#undef MXT_my_vector_
#define MXT_my_vector_ 0x1000
#else
#if MXT_my_vector_ == 0x0000
template <typename T>
class my_vector;
#endif
#endif //MXT_my_vector_
1.7.2.宏展开规则
巧妙使用宏可以简化元编程任务,比如成员函数生成的自动化。我们在这里简单提一下非显而易见的预处理器规则 59 :
-
标记串联运算符## 从两个字符串的串联中生成一个标记。它不仅仅是一个“空白消除”操作符。如果结果不是单个 C++ 标记,则是非法的:
#define M(a,b,c) a ## b ## c int I = M(3,+,2); // error, illegal: 3+2 is not a single token int J = M(0,x,2); // ok, gives 0x2
-
stringizer 前缀# 将文本 60 转换为有效的对应的 C++ 字符串,因此它将插入右反斜杠,依此类推。
-
通常宏展开是递归的。首先,参数被完全展开,然后它们在宏定义中被替换,然后最终结果被再次检查并可能再次展开:
#define A1 100 #define A2 200 #define Z(a,b) a ## b Z(A, 1); // expands to A1, which expands to 100 Z(A, 3); // expands to A3
-
然而,# 和## 这两个运算符,抑制了它们自变量上的,所以:
Z(B, A1); // expands to BA1, not to B100
-
为了确保所有内容都被展开,您可以添加一个额外的间接层,这个层显然什么也不做:
#define Y(a,b) a ## b #define Z(a,b) Y(a,b) Z(B,A1); // expands first to Y(B,A1). Since neither B nor A1 is an operand // of # or ##, they are expanded, so we get Y(B,100), // which in turn becomes B100
-
宏不能递归,所以在扩展 Z 时,不考虑对 Z 的任何直接或间接引用:
#define X Z #define Z X+Z Z; // expands first as X+Z. The second Z is ignored; then the first X // is replaced by Z, and the process stops, // so the final result is "Z+Z"
-
一个流行的技巧是定义一个宏本身。这实际上相当于一个#undef,除了宏仍然被定义(所以#ifdef 和类似的指令不会改变行为)。
#define A A
最后总结一下:
#define A 1
#define X2(a, b) const char* c##a = b
#define X(x) X2(x, #x)
#define Y(x) X(x)
X2(A, "A"); // const char* c##A = "A" const char* cA = "A";
X(A); // X2(A, #A) X2(1, "A") const char* c1 = "A";
Y(A); // X(A) X(1) X2(1, "1") const char* c1 = "1";
注意,在这段代码中,X 可能看起来只是 X2 的一个方便快捷的方式,但不是。通常你不能观察到区别,但是在 X 扩展到 X2 之前,论点扩展发生了,一些直接调用 X2 可以阻止的事情。
用常量(enum 或 static const int)替换定义整数的宏有多安全?答案在前面的代码片段中。更改后,预处理程序的技巧将被打破:
//#define A 1
static const int A = 1;
// ...
X(A); // const char* cA = "A";
Y(A); // const char* cA = "A";
但是如果 A 没有保证是宏,那么替换应该是透明的。 61
另一个值得一提的规则是,预处理程序尊重带参数和不带参数的宏之间的区别。特别是,它不会试图展开一个后跟一个左括号的 A,类似地,对于没有后跟一个括号的 X。这条规则在一个流行的习惯用法中被利用,它防止构造类型 C 62 的未命名实例:
template <typename T>
class C
{
public:
explicit C([[one argument here]]);
};
#define C(a) sizeof(sorry_anonymous_instance_not_allowed_from_ ## a)
C x("argument"); // ok: C not followed by bracket is not expanded
return C("temporary"); // error: the sizeof statement does not compile
最后,由于许多模板类型包含一个逗号,所以通常不可能通过宏安全地传递它们:
#define DECLARE_x_OF_TYPE(T) T x
DECLARE_x_OF_TYPE(std::map<int, double>); /* error:
^^^^^^^^^^^^ ^^^^^^^ two arguments */
对此有几种解决方法:
-
额外的括号(一般来说,这不太可能*起作用,因为在 C++ 中,括号中的类型没有多大用处):
DECLARE_x_OF_TYPE((std::map<int, double>)); // (std::map<int, double>) x; error ```*
** typedef 可以工作,除非该类型依赖于其他宏参数:
```cpp
typedef std::map<int, double> map_int_double;
DECLARE_x_OF_TYPE(map_int_double);
```
* 另一个宏:
```cpp
#define mxt_APPLY2(T, T1, T2) T< T1, T2 >
DECLARE_x_OF_TYPE(mxt_APPLY2(std::map,int,double));
```*
*__________________
1 不严格地说,这就是“元编程”中“元”前缀的原因。
2 在现代 C++ 中有更多,但你可以考虑它们的扩展;这里描述的是元编程一等公民。第十二章有更多细节。
3 通常任何整数类型都被接受,包括命名/匿名 enum、bool、typedefs(像 ptrdiff_t 和 size_t),甚至编译器特定类型(例如 MSVC 的 __int64)。指向成员/全局函数的指针是不受限制的;指向一个变量的指针(有外部链接)是合法的,但是它不能在编译时被解引用*,所以这在实践中有非常有限的用途。参见第十一章。*
链接器最终可能会将它们折叠起来,因为它们可能会产生相同的机器代码,但从语言的角度来看,它们是不同的。
5 一个例外是文字 0 可能不是有效的指针。
6 更完整的讨论见 1.3.6 节和 11.2.2 。
7 参见 1.3.2 节的注释。
8 你可以把一个浮点字面值强制转换成整数,所以严格来说,(int)(1.2)是允许的。并非所有的编译器都严格遵守这条规则。
9 使用 LINE 作为参数在实践中很少出现;它在自动类型枚举(见 7.6 节)和一些自定义断言的实现中很流行。
10 我们必须选择不同的名称,以避免遮蔽外部模板参数 scalar_t
11 另见http://www . open-STD . org/JT C1/sc22/wg21/docs/cwg _ defects . html # 666。
12 即使不是正确的例子,开明的读者可能也要考虑一下 std::string,std::wstring,std::basic_string < T >之间的关系。
13 参见 1.4.9。
14 作为旁注,这再一次说明了在 TMP 中,你写的代码越少越好。
15 与 1.1.1 节中描述的 typename 的使用进行比较
16 参见 http://www.research.att.com/~bs/比雅尼·斯特劳斯特鲁普在他的《C++ 风格与技巧常见问题》中提到的“脆弱基类问题”。
17 确切的规则在**【2】**中有记载和解释。你可以参考这本书,对这里总结的几个段落进行详细的解释。
18 见下一节。
19 这个例子取自[2]。
20 特别是编译器不需要注意到 void f(arg < 2*N >)和 void f(arg < N+N >)是同一个模板函数,这样的双重定义会让程序格式不良。然而,在实践中,大多数编译器会识别出歧义并发出适当的错误。
21 模板函数不能部分专门化,只能重载。
22 不幸的是,一些流行的编译器容忍了这一点。
24 多出来的“Y”不过是诗意的许可。参考 Danny Kalev 在 http://www.informit.com/guides/content.aspx?g=cplusplus[的精彩文章。](http://www.informit.com/guides/content.aspx?g=cplusplus&seqNum=454)
25 除 STD::numeric _ limits:::quiet _ NaN()。
甚至源代码也有生命周期,最终它会“死去”,也就是说,它会被从头重写。然而,设计越稳健,它的寿命就越长,而风格是设计的一部分。参见[5]。
27 如**【5】**所述,通常成员函数名应该是动作。因此,空应该是 make_empty 的同义词,而不是 is_empty 的同义词。但是,STL 约定是成立的,并且被普遍理解。如果有疑问,就像 std::vector 那样做。
28 具体要求和规格见 http://en.cppreference.com/w/cpp/language/constexpr。
最后两种实现的区别很大程度上在于它们对无效语法的反应。作为练习,考虑一些类似 MXT_SORT2(x,y)的恶意代码 if (true)抛出 _ exception。
读者可能想回顾一下本章开头的简单例子。
31 参见章节 6.2.1 。
32 有些编译器,比如 MSVC71,以前有未命名参数的问题;详细示例请参考第 11.3.3 段。
一些作者为此保留了关键字 typename。换句话说,他们声明 template < typename T >来表示 T 是“任何类型”,声明 template < class T >来表示 T 确实是一个类,而不是原生类型。然而,这种区分是相当人为的。
34 注意,这不是正式要求;只是一个名字而已!名字反映了我们对类型的看法;如有必要,稍后我们将强制执行此操作。
35 数学倾向的读者应该把后者看作前者的特例。常量 5 '可以用名为 five 或 static_value < int,5 >的类型来代替。这导致了更大的通用性。更多信息参见**【3】**。
36 它应该驻留在一个匿名的名称空间中,但这并不能使它不可访问。
37 优点在苹果技术说明 TN2185 中有广泛描述;参考下页:【http://developer.apple.com/technotes/tn2007/tn2185.html】。
38 这样的功能在【5】中表示为垫片。
39 如果 a 是长度为 2 的 T 的数组,那么(char *)(&a[1])-(char *)(&a[0])是一个 ptrdiff_t,至少与 sizeof(T)一样大。这意味着 ptrdiff_t 至少和 int 一样大。这个参数实际上表明 sizeof 的每个结果都可以存储在 ptrdiff_t 中。泛型 size_t 可能不会存储在 ptrdiff_t 中,因为 sizeof 不一定是满射的—可能有一个 size_t 值大于每个可能的 sizeof。
40 例如,创建 std::string 的副本所需的时间与字符串本身的长度成正比,所以这不仅取决于类型,还取决于实例;或者,复制一个 double 是一个常量时间操作。从数学上来说,“常数时间”的概念在 C++ 中并没有很好的定义;这个问题太复杂,无法用脚注来说明,但我们将概述一下这个想法。对于任何可能的输入,如果算法的执行时间受常数 K 的限制,则算法是 O(1)。如果可能的输入数量是有限的,即使数量很大,算法也自动为 O(1)。例如,在 C++ 中,两个 int 的和是 O(1)。一般来说,C++ 内存模型具有有限的可寻址空间(因为所有对象都有固定的大小,而“地址”是一个对象),这意味着某些算法的可能输入数量是有限的。快速排序的复杂度是 O(N*log(N)),但是 std::sort 在形式上可能被认为是 O(1),其中——不严格地说——常数 K 是对最大可能的数组进行排序所需的时间。
41 对比 2.3.1 章节。
42 有些对象可能想提前检查覆盖是否可行。例如,如果 T 是 std::string,其 size()= = the . size(),那么它可能能够执行安全的 memcpy。
43 注意,最好的选择是要求配对的对象提供合适的运算符,所以我们委托比较。例如,pair < const char*,int >和 pair < std::string,int >不太可能触发临时字符串的构造,因为我们期望 STL 提供一个 operator==(const char*,const std::string &)。
44c++ 11 中改变了“聚合”的定义,引入了统一初始化。由于这个问题相当复杂和详细,读者可能希望看到参考书目。
编写“完全防弹”的代码可能会增加复杂性的巨大成本。有时,这种复杂性也会抑制一些编译器优化。作为一条规则,程序员应该总是务实地推理,并接受这样一个事实,即代码不会处理每一种可能的极端情况。
46 参见 1.6 节。
47 平台,通常我们指的是集合{处理器、操作系统、编译器、链接器}。
从经验分析来看,有时受保护的空析构函数会抑制优化。一些测量结果已经发表在**【3】**。
49 注意递归< T,-1 >不会编译。
50 大多数编译器都实现了这种优化,至少在单继承的情况下是这样。
【51】MAC OS x 10 . 4 . 8,XCode 2.4.1,GCC 4.01。
52 将警告设置为最高级别只有一次,在最后的开发阶段或搜寻神秘的 bug 时。
53 见标准中 3.7.2。
54 包括这本书的作者。
55 当然,这个规则也有已知的例外:一些 C 运行时函数(sprintf,floor),甚至少数 STL 函数(string::operator+)。
56 无论如何,释放记忆可能是完全不同的一回事。
57 在 AMD 处理器上,double 应该对齐到一个 8 字节边界;否则,CPU 将执行多个不必要的加载操作。在不同的处理器上,访问未对齐的 double 可能会立即导致程序崩溃。
58 一个普遍的策略是把记忆分成几块,用某种程度的懒惰来释放它们。
59 要想获得完整的参考,可以考虑 GNU 手册【http://gcc.gnu.org/onlinedocs/cpp.pdf】的。
60 它只能应用于宏参数,不能应用于任意文本。
61 在这种情况下,它们应该用作枚举。特别是,如果它们碰巧是宏,那么取消对它们的定义,并用真实的枚举替换它们应该是安全的。
62 这个例子其实只有看完 2,2 节才会清楚。****