C++ 高级元编程(七)
6# 八、函子
这一章重点介绍了几种在编写(或不编写)函子时有帮助的技术。
大多数 STL 算法需要编译时函数对象,这通常需要一些手动编码:
struct Person
{
unsigned int age;
std::string home_address;
double salary() const;
};
std::vector<Person> data;
std::sort(data.begin(), data.end(), /* by age */ );
std::partition(data.begin(), data.end(), /* by salary */ );
如果可以修改 Person,有时一个优雅而快捷的解决方案是编写一个公共静态成员函数 和一个成员仿函数。这同时实现了最高的效率和控制,因为您的代码可以访问私有成员:
struct Person
{
private:
unsigned int age;
public:
static bool less_by_age(const Person& a, const Person& b)
{
return a.age < b.age;
}
struct BY_AGE
{
bool operator()(const Person& a, const Person& b) const
{
return Person::less_by_age(a, b);
}
};
};
std::vector<Person> data;
std::sort(data.begin(), data.end(), Person::less_by_age); // suboptimal
std::sort(data.begin(), data.end(), Person::BY_AGE()); // good
静态成员函数可以访问私有数据。然而,编译器内联比较要困难得多,所以函子通常更好。
您甚至可以提取一些代码,将前者转换为后者:
template <typename T, bool (*LESS)(const T&, const T&)>
struct less_compare_t
{
typedef T first_argument_type;
typedef T second_argument_type;
typedef bool result_type;
bool operator()(const T& x, const T& y) const
{
return LESS(x, y);
}
};
struct Person
{
private:
unsigned int age;
public:
static bool less_by_age(const Person& a, const Person& b)
{
return a.age < b.age;
}
typedef less_compare_t<Person, Person::less_by_age> BY_AGE;
};
选择函数/仿函数的名称是为了使表达式在实例化时清晰,而不是在定义时清晰。
注意,非泛型函子(其参数具有固定类型)通常是该类的成员。
假设函子可以自由复制并通过值传递,这通常是合理的。如果一个函子需要许多数据成员,你最好把它们收集在一个单独的结构中,只存储一个引用。函子的调用方将负责保存额外的信息:
struct information_needed_to_sort_elements
{
// ...
};
class my_less
{
const information_needed_to_sort_elements& ref_;
public:
explicit functor(const information_needed_to_sort_elements& ref)
: ref_(ref)
{
}
bool operator()(const Person& p1, const Person& P2) const
{ ... }
};
int main()
{
information_needed_to_sort_elements i;
// build a suitable container data...
std::sort(data.begin(), data.end(), my_less(i));
}
STL 算法不提供任何关于函数对象副本数量的保证。
另一个有趣的特性是函子静态类型是不相关的,因为它总是被推导出来的。如果函子是从一个函数返回的,它将被立即使用(见 4.3.4 节);如果它被传递给一个函数模板,它将绑定到一个接受任何东西的参数。
这允许客户端在调用点生成复杂函数对象的匿名实例:
i = std::find_if(begin, end, std::bind2nd(std::less<double>(), 3.14));
// the exact type of the functor is irrelevant
// since find_if has an argument that binds to anything:
// template <typename I, typename F>
// I find_if(I begin, I end, F func)
注意 C++0x 包括对 lambda 对象创建的支持。
这是一种新的语法,可以传递花括号中的匿名“代码片段”,就像它们是函子一样。这减轻了名称污染的问题。换句话说,没有必要给一个不被重用的实体命名。
更多详情见第 12.4 节。
8.1.强函子和弱函子
有些函子是强类型的。这意味着用户在确定模板参数时固定函数调用的参数。所有标准泛函都是强类型的。
template <typename T>
struct less
{
bool operator()(const T& lhs, const T& rhs) const
{
return lhs < rhs;
}
};
std::sort(data.begin(), data.end(), less<Person>());
或者,您可以有一个弱 仿函数,它可以更自由地接受参数 1 :
struct weak_less
{
template <typename T>
bool operator()(const T& lhs, const T& rhs) const
{
return lhs < rhs;
}
};
std::sort(data.begin(), data.end(), weak_less());
强类型函子静态阻塞所有与 T 不兼容的类型,但由于这仅限于接口,它实际上可以与弱函子共享实现:
template <typename T>
struct less : private weak_less
{
bool operator()(const T& lhs, const T& rhs) const
{
return static_cast<const weak_less&>(*this)(lhs, rhs);
}
};
8.2.仿函数合成工具
STL 提供了组合函子和值的工具。例如,std::bind2nd 将一个二元运算和一个操作数转换为一元函数。通常,您需要执行相反操作的工具。
by age 中的前缀 by 实际上是一个二元关系与一个存取子的复合。年龄提取一个人的年龄,并比较两个年龄。这里有一个抽象这个组合概念的最小实现。
template <typename functor_t>
class by_t
{
functor_t f_;
public:
by_t(functor_t f)
: f_(f)
{}
template <typename argument_t>
bool operator()(const argument_t& a, const argument_t& b) const
{
return f_(a) < f_(b);
}
};
template <typename functor_t>
inline by_t<functor_t> by(const functor_t& f)
{
return f;
}
// see Section 1.1.4
template <typename R, typename A>
inline by_t<R (*)(A)> by(R (*f)(A))
{
return f;
}
struct age_t
{
unsigned int operator()(const Person& p) const
{
return p.age;
}
age_t(int = 0)
{
}
};
static const age_t AGE = 0<sup class="calibre7">2</sup>;
int main()
{
std::vector<Person> data;
std::sort(data.begin(), data.end(), by(AGE));
}
by 是一个仿函数合成工具。由于它对 functor_t 没有任何要求,所以它会接受合适的静态成员函数,如果 Person::age 是私有的,这就很方便了:
struct Person
{
private:
unsigned int age;
public:
static int AGE(const Person& p)
{
return p.age;
}
};
std::sort(data.begin(), data.end(), by(Person::AGE)); // ok!
函子/访问器可能被赋予强大的λ语义。
这里是第 9.2 节的另一个预览。在伪直观表示法中,比较器(A,S)是一个谓词,如果 A(O)比 S“小”,则它在对象 O 上返回 true。“小”是一个通用的二元谓词。
template
<
typename scalar_t,
typename accessor_t,
template <typename T> class less_t
>
class comparator
{
scalar_t x_;
accessor_t a_;
public:
comparator(scalar_t x, accessor_t a = accessor_t())
: x_(x), a_(a)
{
}
template <typename argument_t>
bool operator()(const argument_t& obj) const
{
less_t<scalar_t> less_;
return less_(a_(obj), x_);
}
};
使用模板-模板参数 代替普通的二元谓词,可以避免两次键入 scalar_t,并使匿名实例非常清晰易读:
comparator<double, SALARY, std::greater>(3.0)
另一个小问题是类的布局:x_ 是在 a_ 之前声明的,因为 a_ 通常是无状态的,因此是一个小对象。x_ 可能有更强的对齐约束。
现在,您可以向仿函数添加运算符,并将其提升为 lambda 谓词 3 :
struct age_t
{
int operator()(const Person& a) const
{
return a.age;
}
template <typename T>
comparator<T,age_t,std::less> operator<(const T& x) const
{
return comparator<T,age_t,std::less>(x, *this);
}
template <typename T>
comparator<T,age_t,std::equal_to> operator==(const T& x) const
{
return comparator<T,age_t,std::equal_to>(x, *this);
}
};
std::partition(data.begin(), data.end(), Person::AGE < 35);
std::partition(data.begin(), data.end(), Person::AGE == 18);
只需一点努力,您就可以向链接操作符添加更多的语法技巧:
const selector<true> INCREASING;
const selector<false> DECREASING;
template <typename T>
bool oriented_less(const T& x, const T& y, selector<true>)
{
return x<y;
}
template <typename T>
bool oriented_less(const T& x, const T& y, selector<false>)
{
return y<x;
}
oriented_less 可以翻转操作符< and simulate operator>。
template <typename functor_t, bool ASCENDING = true>
class by_t
{
functor_t f_;
public:
by_t(functor_t f) : f_(f) {}
template <typename argument_t>
bool operator()(const argument_t& a, const argument_t& b) const
{
return oriented_less(f_(a), f_(b), selector<ASCENDING>());
}
// inversion operators:
by_t<functor_t, true> operator+() const
{
return f_;
}
by_t<functor_t, false> operator-() const
{
return f_;
}
};
最后,还有另一个辅助函数 :
template <bool DIRECTION, typename functor_t>
by_t<functor_t, DIRECTION> by(selector<DIRECTION>, const functor_t& v)
{
return by_t<functor_t, DIRECTION>(v);
}
所有这些都允许写作:
std::sort(data.begin(), data.end(), +by(Person::AGE));
std::sort(data.begin(), data.end(), -by(Person::AGE));
std::sort(data.begin(), data.end(), by(DECREASING, Person::AGE));
注意我选择运算符+和运算符-是因为 by 处理的是数字属性;一元谓词的逻辑倒置最好用运算符来表达!
此外,第 2 行和第 3 行是相同的。挑最清晰的只是风格问题。
对 by_t 的最后一个改进是在 operator() 中执行严格的类型检查。
函数调用操作符几乎接受任何东西,所以更多的类型检查将会捕获那些仅仅是偶然编译的代码所产生的错误:
std::vector<Animal> data;
std::sort(data.begin(), data.end(), by(Person::AGE));
一种方便的方法是利用函子的合作。如果 functor_t 有一个成员 argument_type,它也将是一个强运算符()的参数。否则,使用弱函数调用操作符。
通常,您将决策隐藏在模板参数 中,并提供两个局部专门化。首先,一些特质:
template <typename T>
struct argument_type_of
{
typedef typename T::argument_type type;
};
template <typename A, typename R>
struct argument_type_of<R (*)(A)>
{
typedef A type;
};
template <typename A, typename R>
struct argument_type_of<R (*)(const A&)>
{
typedef A type;
};
template <typename T>
struct has_argument_type
: selector<[[ true if T::argument_type exists<sup class="calibre7">4</sup> ]]>
{
};
template <typename A, typename R>
struct has_argument_type<R (*)(A) >
: selector<true>
{
};
// ...
第一个专门化执行严格的类型检查。
template
<
typename functor_t,
bool ASCENDING = true,
bool STRICT_CHECK = has_argument_type<functor_t>::value
>
struct by_t;
template <typename functor_t, bool ASCENDING>
struct by_t<functor_t, ASCENDING, true>
{
// ...
typedef typename argument_type_of<functor_t>::type argument_type;
// note: strong argument type
bool operator()(const argument_type& a, const argument_type& b) const
{
return oriented_less(f_(a), f_(b), selector<ASCENDING>());
}
};
template <typename functor_t, bool ASCENDING>
struct by_t<functor_t, ASCENDING, false>
{
// ...
// note: weak argument type. This will accept anything
template <typename argument_t>
bool operator()(const argument_t& a, const argument_t& b) const
{
return oriented_less(f_(a), f_(b), selector<ASCENDING>());
}
};
为了最大限度地减少代码重复,您在模板库中提取出函数调用操作符,并使用 static_cast,如 CRTP 所示:
template <typename functor_t, bool ASCENDING = true>
struct by_t;
template <typename functor_t, bool ASCENDING, bool STRICT_CHECK>
struct by_base_t;
template <typename functor_t, bool ASCENDING>
struct by_base_t<functor_t, ASCENDING, true>
{
const functor_t& f() const
{
typedef by_t<functor_t, ASCENDING> real_type;
return static_cast<const real_type&>(*this).f_;
}
typedef typename argument_type_of<functor_t>::type argument_type;
bool operator()(const argument_type& a, const argument_type& b) const
{
return oriented_less(f()(a), f()(b), selector<ASCENDING>());
}
};
template <typename functor_t, bool ASCENDING>
struct by_base_t<functor_t, ASCENDING, false>
{
const functor_t& f() const
{
typedef by_t<functor_t, ASCENDING> real_type;
return static_cast<const real_type&>(*this).f_;
}
template <typename argument_t>
bool operator()(const argument_t& a, const argument_t& b) const
{
return oriented_less(f()(a), f()(b), selector<ASCENDING>());
}
};
template <typename functor_t, bool ASCENDING = true>
struct by_t
: by_base_t<functor_t,ASCENDING,has_argument_type<functor_t>::value>>
{
// ...
};
8.3.内部模板函子
仿函数包装器可以用作接口杠杆工具。
从语法上讲,您利用了内部类模板知道外部类的模板参数这一事实。
8.3.1.函数到函子的转换
为简单起见,假设您有一个函数集合 ,具有相似的签名 T f(T,T,...T),其中参数的数量是变化的。进一步假设要执行的函数列表在运行时是已知的,那么您需要一个带有虚拟调用的基类,它的唯一签名可以是(const T*,size_t)。 5
让我们寻找一种自动执行转换的方法:
template <typename T>
struct base
{
virtual T eval(const T*, size_t) const = 0;
virtual ~base() {}
};
给定一个函数,比如 double F(double,double),你可以把它嵌入到一个函子中,但是你必须同时推导出 T 和 F:
template <typename T, T (*F)(T,T)>
struct functor : public base<T>
{
// ...
};
实际上,在 F 之前需要 T,所以可以只在 T 上构建一个类模板,然后是一个内部模板类:
template <typename T>
struct outer
{
template <T (*F)(T,T)>
struct inner : public base<T>
{
首先你识别外部,然后你建立内部:
template <typename T>
struct function_call_traits
{
template <T (*F)()>
struct eval_0 : public base<T>
{
virtual T eval(const T* , size_t) const { return F(); }
};
template <T (*F)(T)>
struct eval_1 : public base<T>
{
virtual T eval(const T* x, size_t) const { return F(x[0]); }
};
template <T (*F)(T, T)>
struct eval_2 : public base<T>
{
virtual T eval(const T* x, size_t) const { return F(x[0], x[1]); }
};
// ...
template <T (*F)()>
eval_0<F>* get_ptr() const
{
return new eval_0<F>;
}
template <T (*F)(T)>
eval_1<F>* get_ptr() const
{
return new eval_1<F>;
}
template <T (*F)(T, T)>
eval_2<F>* get_ptr() const
{
return new eval_2<F>;
}
// ...
};
template <typename T>
inline function_call_traits<T> get_function_call(T (*F)())
{
return function_call_traits<T>();
}
template <typename T>
inline function_call_traits<T> get_function_call(T (*F)(T))
{
return function_call_traits<T>();
}
template <typename T>
inline function_call_traits<T> get_function_call(T (*F)(T, T))
{
return function_call_traits<T>();
}
// ...
#define MXT_FUNCTION_CALL_PTR(F) get_function_call(F).get_ptr<F>()
请注意:
- f 使用了两次,第一次作为指针,第二次作为模板参数。
- get_ptr 函数不是静态的,看起来可能有点奇怪,这是一个实际上要实例化的 traits 类的例子(但是匿名使用)。
double add0()
{
return 6.28;
}
double add1(double x)
{
return x+3.14;
}
double add2(double x, double y)
{
return x+y;
}
int main()
{
double x[5] = {1,2,3,4,5};
base<double>* f[3] =
{
MXT_FUNCTION_CALL_PTR(add0),
MXT_FUNCTION_CALL_PTR(add1),
MXT_FUNCTION_CALL_PTR(add2)
};
for (int i=0; i<3; ++i)
std::cout << f[i]->eval(x, 5);
// normal destruction code has been omitted for brevity
}
前面的示例通过调用同一个接口来执行 add0()、add1(x[0])和 add2(x[0],x[1])。
8.3.2.成员 到函子的转换
上一节中看到的相同技术可以将指针转换成函子。 6
在 C++ 中,没有访问限制的简单结构通常用于传输小块数据。理想情况下,您会希望保持这种简单性,并且能够编写没有开销的代码:
struct Person
{
unsigned int age;
double salary() const;
};
std::vector<Person> data;
// warning: pseudo-c++
std::sort(data.begin(), data.end(), by(Person::age));
std::sort(data.begin(), data.end(), by(Person::salary));
因为可以使用指向成员的指针作为模板参数,所以编写一个辅助包装器并不太难。不幸的是,实例化太冗长,没有用。
template <typename from_t, typename to_t, to_t from_t::* POINTER>
struct data_member
{
const to_t& operator()(const from_t& x) const
{
return x.*POINTER;
}
};
template <typename from_t, typename to_t, to_t (from_t::*POINTER)() const>
struct property_member
{
to_t operator()(const from_t& x) const
{
return (x.*POINTER)();
}
};
struct TEST
{
int A;
int B() const { return -A; }
};
TEST data[3] = {2,1,3};
// very verbose...
std::sort(data, data+3, by(data_member<TEST, int, &TEST::A>()));
std::sort(data, data+3, by(property_member<TEST, int, &TEST::B>()));
然而,不可能编写一个泛型类指针作为唯一的模板参数:
template <typename A, typename B, B A::*POINTER>
struct wrapper<POINTER> // illegal: not c++
您必须再次求助于嵌套的类模板:
template <typename from_t, typename to_t>
struct wrapper
{
template <to_t from_t::*POINTER> // legal!
struct dataptr_t
{
const to_t& operator()(const from_t& x) const
{
return x.*POINTER;
}
};
template <to_t from_t::*POINTER>
dataptr_t<POINTER> get() const
{
return dataptr_t<POINTER>();
}
};
template <typename from_t, typename to_t>
wrapper<from_t, to_t> get_wrapper(to_t from_t::* pointer)
{
return wrapper<from_t, to_t>();
}
该示例包括一个函数,该函数使用指针执行第一次演绎,同样,您必须提供两次相同的指针,一次在运行时(其值基本上被忽略,但其类型用于演绎),另一次在编译时:
#define MEMBER(PTR) get_wrapper(PTR).get<PTR>()
- get_wrapper 自动从 PTR 中推导出参数 T1 和 T2,所以 get_wrapper(PTR)将返回包装器。
- 然后您要求这个包装器在 PTR 上再次实例化它的成员函数 get,它返回正确的对象。
如果 PTR 有 int TEST::*,这个宏将产生一个 dataptr_t 类型的函子(技术上来说,包装器:dataptr _ t)。
但是,任何其他重载都可以。这是一个扩展版本:
template <typename from_t, typename to_t>
struct wrapper
{
template <to_t from_t::* POINTER>
struct dataptr_t
{
// optional:
// typedef from_t argument_type;
const to_t& operator()(const from_t& x) const
{
return x.*POINTER;
}
};
template <to_t (from_t::*POINTER)() const>
struct propptr_t
{
// optional:
// typedef from_t argument_type;
to_t operator()(const from_t& x) const
{
return (x.*POINTER)();
}
};
template <to_t from_t::* POINTER>
dataptr_t<POINTER> get() const
{
return dataptr_t<POINTER>();
}
template <to_t (from_t::*POINTER)() const>
propptr_t<POINTER> get() const
{
return propptr_t<POINTER>();
}
};
template <typename from_t, typename to_t>
wrapper<from_t, to_t> get_wrapper(to_t from_t::* pointer)
{
return wrapper<from_t, to_t>();
}
template <typename from_t, typename to_t>
wrapper<from_t, to_t> get_wrapper(to_t (from_t::*pointer)() const)
{
return wrapper<from_t, to_t>();
}
#define mxt_create_accessor(PTR) get_wrapper(PTR).get<PTR>()
struct TEST
{
int A;
int B() const { return -A; }
};
TEST data[3] = {2,1,3};
std::sort(data, data+3, by(mxt_create_accessor(&TEST::A)));
std::sort(data, data+3, by(mxt_create_accessor(&TEST::B)));
和往常一样,如果类名中包含逗号(比如 std::map ),那么需要在调用宏之前对其进行 typedef。
&不是绝对必要的。可以将宏重新定义为 get_wrapper(PTR)。get ()以便在普通限定名上调用它。
根据标准,宏不能在模板中工作。编译器需要一个额外的 template 关键字来正确推断 get 是什么,所以最好的选择是定义第二个名为(比方说)的宏
mxt_create_accessor_template
get_wrapper(PTR).template get<&PTR>()
每当 PTR 依赖于对宏展开的行有影响的模板参数时,就需要使用这个版本。另一方面,当 PTR 不依赖于任何其他东西时,它是被禁止的。 7
8.3.3.关于双层包装技术的更多信息
在上一段中,您看到了一个类似如下的宏:
#define MEMBER(PTR) get_wrapper(PTR).get<PTR>()
参数 PTR 使用了两次——第一次作为模板函数的参数,忽略其值,只使用其类型,返回一个“中间函子”;第二次作为仿函数本身的模板参数,它产生你需要的最终对象。
让我们修改这个技巧,来面对一个明显不相关的问题。 8 在经典 C++ 中,枚举值自动衰减为整数。这可能会导致错误:
enum A { XA = 1 };
enum B { XB = 1 };
int main()
{
A a = XA;
B b = XB;
a == b; // compiles and returns true, even if enums are unrelated
}
让我们介绍一个简单的帮助函子:enum_const 类型的对象是一个静态值,它与来自相同(非匿名)枚举的一个值完全相等,但不能与整数或不同类型进行比较。
template <typename T, T VALUE>
struct enum_const
{
bool operator==(T that) const
{
return VALUE == that;
}
// Barton-Nackman, see section 6.6
friend inline bool operator==(T lhs, enum_const<T, VALUE> rhs)
{
return rhs == lhs;
}
};
template <typename T>
struct enum_const_helper
{
template <T VALUE>
enum_const<T, VALUE> get() const
{
return enum_const<T, VALUE>();
}
};
template <typename T>
inline enum_const_helper<T> wrap(T)
{
return enum_const_helper<T>();
}
所以你可以写这样的代码:
#define enum_static_const(X) wrap(X).get<X>()
int main()
{
A a = XA;
B b = XB;
a == b; // ok
b == enum_static_const(XA); // error
enum_static_const(XB) == a; // error
}
error: invalid operands to binary expression ('int' and 'enum_const<A, (A)1U>')
b == enum_static_const(XA); // fails
~ ^ ~~~~~~~~~~~~~~~~~~~~~
note: candidate template ignored: deduced conflicting types for parameter 'T' ('B' vs. 'A')
inline bool operator==(T lhs, enum_const<T, VALUE> rhs)
^
error: invalid operands to binary expression ('enum_const<B, (B)1U>' and 'int')
enum_static_const(XB) == a; // fails
~~~~~~~~~~~~~~~~~~~~~ ^ ~
note: candidate function not viable: no known conversion from 'A' to 'B' for 1st argument;
bool operator==(T that) const
编写的宏可以工作,但是它需要 X 是一个编译时常数:
#define enum_static_const(X) wrap(X).get<X>()
让我们寻找一个解决方法。第一个问题是,wrap 能检测出 X 是常数还是变量吗?它可以部分地—一个变量可以绑定到一个引用。 9
template <typename T>
inline enum_const_helper<T> wrap(T, ...)
{
return enum_const_helper<T>();
}
template <typename T>
inline enum_var_helper<T> wrap(T& x, int)
{
return enum_var_helper<T>(x);
}
注意包装的附加参数。假设 X 是一个变量,你写 wrap(X);wrap(T &)和 wrap(T)都是有效匹配,因此重载决策是不明确的。另一方面,表达式 wrap(X,0)在可能的情况下会倾向于匹配(T &,int),因为 0 正好有 int 类型(这比省略号好)。所以宏变成了:
#define enum_static_const(X) wrap(X, 0).get<X>()
第二个问题是,如果 X 是一个变量,能否给一个意义得到()?
同样,让我们引入一个 int 类型的伪参数:
template <typename T>
struct enum_const_helper
{
template <T VALUE>
enum_const<T, VALUE> get(int) const
{
return enum_const<T, VALUE>();
}
};
这是宏的最终版本:
#define enum_static_const(X) wrap(X, 0).get<X>(0)
现在语法不同了:get 可能是成员对象,get (0) 实际上是(get.operator < (X))。操作员> (0)。这是有效的,因为 wrap 返回的对象不依赖于其他模板参数。
下面是缺失的一段代码:
template <typename T>
struct enum_var
{
const T value_;
explicit enum_var(T val)
: value_(val) {}
bool operator==(T that) const
{
return value_ == that;
}
// Barton-Nackman again
friend inline bool operator==(T lhs, enum_var<T> rhs)
{
return rhs == lhs;
}
enum_var operator<(T) const // dummy operator<
{ return *this; }
enum_var operator>(int) const // dummy operator>
{ return *this; }
};
template <typename T>
struct enum_var_helper
{
enum_var<T> get; // surprise: data member called get
enum_var_helper(T& x)
: get(x) {}
};
enum_static_const(XB) == b; // picks enum_const<B,1>::operator==(b)
enum_static_const(b) == XB; // picks enum_var<B>(b).operator==(XB)
8.4.累积
累加器 是一个函子,它对一系列元素执行逻辑“传递”,并通过 operator+=或 operator+更新。这在 STL 算法 std::accumulate 中实现。
template <typename iterator_t, typename accumulator_t>
accumulator_t accumulate(iterator_t b, iterator_t e, accumulator_t x)
{
while (b != e)
x = x + *(b++);
return x;
}
如果 x 是 value_type(0),这实际上产生了范围上的和。
累加器可以分为在线的和离线的*。脱机对象在一个范围内只能累积一次,不能再添加更多的值。另一方面,在线对象可以累积不相交的范围。(普通总和是一个在线累加过程,因为新的总和只取决于以前的总和以及新的值。精确的百分位数将是一个离线过程,因为两个不相交范围内的第 P 个百分位数同时取决于所有的值。 10 )*
概括的第一步是累加 F(i),不一定是i. 11
template <typename T>
struct identity
{
T operator()(T x) const { return x; }
};
template <typename iter_t, typename accumulator_t, typename accessor_t>
accumulator_t accumulate(iter_t b, iter_t e, accumulator_t x, accessor_t F)
{
while (b != e)
x = x + F(*(b++));
return x;
}
template <typename iter_t, typename accumulator_t>
accumulator_t accumulate(iterator_t b, iterator_t e, accumulator_t x)
{
return accumulate(b, e, x,
identity<typename std::iterator_traits<iter_t>::reference>());
}
使用 TMP,可以动态构建多层累加器 :
- 识别一组相似的操作,这些操作通过同时执行而不是顺序执行来提高性能。 12
- 为实例化一个未命名的多重累加器定义一个合理的语法。
- 为提取结果定义一个合理的语法。
8.4.1.逐步实施
本节的其余部分将编写一个合适的名为 collect 的函数,该函数可以编写以下内容:
// collect F(*i) for each i in the range
// and produce sum, gcd and max
std::accumulate(begin, end, collect(F)*SUM*GCD*MAX)
您将利用 std::accumulate 返回累加器来一次转储一个或多个所需结果的事实:
int data[7] = { ... };
int S = std::accumulate(data, data+7, collect(identity<int>())*SUM).result(SUM);
int sum, gcd, max;
std::accumulate(begin, end, collect(F)*SUM*GCD*MAX)
.result(SUM >>sum, GCD >>gcd, MAX >>max);
让我们从头开始。
首先,确定基本操作并给每个操作分配一个代码:
enum
{
op_void, // null-operation
op_gcd,
op_max,
op_min,
op_sum
};
同样,您将使用模板旋转。主对象包含操作列表;它首先执行第一个,然后循环列表并调度执行。t 是访问器。
template <typename T, int O1 = op_void, int O2 = op_void,..., int On = op_void>
class accumulate_t
{
typedef accumulate_t<T, O2, O3, ..., On > next_t; // rotation
static const int OP_COUNT = 1+next_t::OP_COUNT;
scalar_t data_[OP_COUNT];
static void apply(/* ... */)
{
// perform operation O1 and store result in data_[0]
// then...
next_t::apply(...);
}
};
然后实现二元运算(为简洁起见,省略了一些代码):
template <int N>
struct op_t;
template <>
struct op_t<op_void>
{
private:
explicit op_t(int = 0) {}
};
template <>
struct op_t<op_sum>
{
explicit op_t(int = 0) {}
template <typename scalar_t>
scalar_t operator()(const scalar_t a, const scalar_t b) const
{
return a+b;
}
};
你创建一些全局常量对象;显式构造函数正是出于这个目的。
const op_t< op_gcd > GCD(0);
const op_t< op_sum > SUM(0);
const op_t< op_max > MAX(0);
const op_t< op_min > MIN(0);
注意没人能构造 op_t <op_void>。</op_void>
因为您可以执行四个不同的操作,所以您将四个作为模板参数的限制:
template
<
typename accessor_t,
int O1 = op_void, int O2 = op_void, int O3 = op_void, int O4 = op_void
>
class accumulate_t
{
typedef typename accessor_t::value_type scalar_t;
typedef accumulate_t<accessor_t,O2,O3,O4> next_t;
template <typename T, int I1, int I2, int I3, int I4>
friend class accumulate_t;
static const int OP_COUNT = 1 + next_t::OP_COUNT;
scalar_t data_[OP_COUNT];
size_t count_;
accessor_t accessor_;
每个对象都是通过访问器的一个实例构造的:
public:
accumulate_t(const accessor_t& v = accessor_t())
: accessor_(v), count_(0), data_()
{
}
// more below...
};
您有一个名为 data_ 的结果数组。第 I 个操作会将其结果存储在 data_[i]中。
递归计算部分确实很简单。有一个公共操作符+ =调用私有静态成员函数:
template <typename object_t>
accumulate_t& operator+=(const object_t& t)
{
apply(data_, accessor_(t), count_); // <-- static
return *this;
}
和一个全局运算符+:
template <typename accessor_t, int N1, ..., int N4, typename scalar_t>
accumulate_t<accessor_t,N1,N2,N3,N4>
operator+(accumulate_t<accessor_t,N1,N2,N3,N4> s, const scalar_t x)
{
return s += x;
}
accessor_(t)得出要在存储单元*data 上累加的值。如果 count 为 0,这意味着单元格是“空的”,只需写入值。否则,调用合并前一个单元格值和新值的第一个二元运算。然后,将指针前进到下一个单元格,并将调用转发到 next_t:
static void apply(scalar_t* const data, const scalar_t x, size_t& count)
{
*data = (count>0) ? op_t<O1>()(*data, x) : x;
next_t::apply(data+1, x, count);
}
当所有操作都是 op_void 时,递归停止。此时,您更新了计数器。
template <typename accessor_t>
class accumulate_t <accessor_t, op_void, op_void, op_void, op_void>
{
/* ... */
static const int OP_COUNT = 0;
static void apply(scalar_t* const, const scalar_t, size_t& count)
{
++count;
}
您需要另一个静态递归来检索结果:
private:
template <int N>
static scalar_t get(const scalar_t* const data, op_t<N>)
{
return O1==N ? data[0] : next_t::get(data+1, op_t<N>());
}
public:
template <int N>
scalar_t result(op_t<N>) const
{
return get(data_, op_t<N>());
}
递归停止器不应被调用。然而,这是必要的,因为 next_t::get 被提到了(因此,无论如何都是完全编译的)。只有当请求类型为 accumulate_t <k1...kn>的对象的结果(op_t )并且 K 不在列表中时,它才会被执行。</k1...kn>
在这种情况下,您可以引发任何合适的运行时错误:
template <typename accessor_t>
class accumulate_t <accessor_t, op_void, op_void, op_void, op_void>
{
private:
template <int N>
static scalar_t get(const scalar_t* const, op_t<N>)
{
// if you prefer,
// throw std::runtime_error("invalid result request");
return std::numeric_limits<scalar_t>::quiet_NaN();
}
public:
/* nothing here */
};
因为 SUM 是一个正确类型的全局常量,所以您最终将调用 std::accumulate(begin,end,[...]).结果(总和)。
此时,您可以编写计算结果的代码和检索结果的代码,但是您仍然缺少累加器工厂。正如基于模板旋转的所有对象经常发生的那样,您给用户一个助手函数,该函数最初产生一个“空累加器”(即 accumulate_t ,或者更准确地说,accumulate_t <t ...="">),这个空对象可以与一个或多个 op_t 重复组合。换句话说:有一个操作符将 accumulate_t 和操作 N1 组合在一起,执行一个静态“push-front”并返回 accumulate_t 。
如果选择运算符* (二进制乘法)进行链接,函数如下所示:
template <int N, int N1, ... int Nk>
accumulate_t<T, N, N1, N2,..,Nk-1> operator*(accumulate_t<T, N1,..,Nk-1, Nk>, op_t<N>)
这个链接操作符将包含一个静态断言,以确保“丢弃的术语”Nk 是 op_void。
下面是全局助手函数:
template <typename accessor_t>
inline accumulate_t<accessor_t> collect(const accessor_t& v)
{
return v;
}
最后,这里是整个类的列表,与递归停止特殊化并列:
| 模板<类型名称 accessor_t,int O1 = op_void,int O2 = op_void,int O3 = op_void,int O4 = op_void>班级积聚{typedef typename accessor _ t::value _ type 标量 _ t;模板好友类 accumulate _ t; | 模板<类型名称 accessor_t>classaccumulate_t{typedef typename accessor _ t::value _ type 标量 _ t;模板好友类 accumulate _ t; | | typedef(类型定义)accumulate _ t<accessor_t>next _ t;</accessor_t> | | | 静态 const int OP _ COUNT = 1+next _ t::OP _ COUNT; | 静态常量 int OP _ COUNT = 0; | | scalar _ t data _[OP _ COUNT];size _ t count _accessor _ t accessor _ | accessor _ t accessor _ | | 静态 void 应用(scalar_t* const 数据,常量标量 x,大小 t&计数){data = (count>0)?op_t ()(data,x):x;next_t::apply(data+1,x,count);} | 静态 void 应用(scalar_t const,常量标量 t,大小 t&计数){++ 计数;} | | 模板 static scalar _ t get(const scalar _ t * const data,op_t{return O1==N?data[0] : next_t::get(data+1,op _ t());} | 模板 static scalar _ t get(const scalar _ t * const,op_t{断言(假);返回 0;} | | 公共: | 公共: | | accumulate_t(常量访问器 _t& v =访问器 t()):访问器 (v),count(0),data(){} | accumulate_t(常量访问器 _t& v =访问器 _t()):访问器 _(v){} | | 模板积累 _t 运算符 (op_t )常量{mxt _ assert(O4 = = op _ void);return 访问器 _;} | 模板积累 _t 运算符* (op_t )常量{return 访问器 ;} | | 模板accumulate_t& operator+=(常量对象 t& t){apply(data,accessor(t),count _);返回* this} | 模板accumulate_t& operator+=(常量对象 t& t){返回* this} | | 模板 scalar_t 结果(op_t )常量{return get(data,op _ t());} | | | size_t size()常量{返回计数 _;} | | | }; | }; |
最后一个特性提供了一次检索更多结果的能力。这是非常重要的,因为它避免了存储累积的结果。
您只需引入一个将引用绑定到每个 op_t 的操作符(本例使用操作符>>,因为它类似于一个箭头)。另一个可能的选择是运算符<=, since <= can be seen as ←) and builds a reference wrapper of unique type. From this temporary, an overloaded accumulator::result 将提取两个操作数并执行赋值。
RESULT1 r1;
RESULT2 r2;
accumulator.result(SUM >> r1, MAX >> r2);
实现如下:
template <typename scalar_t, int N>
struct op_result_t
{
scalar_t& value;
op_result_t(scalar_t& x)
: value(x)
{
}
};
template <typename scalar_t, int N>
inline op_result_t<scalar_t, N> operator>> (const op_t<N>, scalar_t& x)
{
return op_result_t<scalar_t, N>(x);
}
然后将这些方法添加到通用模板中(宏只是为了简洁):
#define ARG(J) const op_result_t<scalar_t, N##J> o##J
// ARG(1) expands to "const op_result_t<scalar_t, N1> o1"
template <int N1>
const accumulate_t& result(ARG(1)) const
{
o1.value = result(op_t<N1>());
return *this;
}
template <int N1, int N2>
const accumulate_t& result(ARG(1), ARG(2)) const
{
result(o2);
return result(o1);
}
template <int N1, int N2, int N3>
const accumulate_t& result(ARG(1), ARG(2), ARG(3)) const
{
result(o3);
return result(o1, o2);
}
template <int N1, int N2, int N3, int N4>
const accumulate_t& result(ARG(1), ARG(2), ARG(3), ARG(4)) const
{
result(o4);
return result(o1, o2, o3);
}
#undef ARG
表达式 MAX>>x 默默返回 op_result_t (x)。
如果 x 与累积结果的类型不同,它将不会编译。
几个额外的增强将节省一些打字。您只需添加第一个结果,并通过操作符()链接后续调用,而不是有许多结果。 十三
template <int N1>
const accumulate_t& result(const op_result_t<scalar_t,N1> o1) const
{
o1.value = result(op_t<N1>());
return *this;
}
template <int N1>
const accumulate_t& operator()(const op_result_t<scalar_t,N1> o1) const
{
return result(o1);
}
所以与其说:
int q_sum, q_gcd, q_max;
std::accumulate(...).result(SUM >> q_sum, GCD >> q_gcd, MAX >> q_max);
新语法是:
std::accumulate(...).result(SUM >> q_sum)(GCD >> q_gcd)(MAX >> q_max);
或者甚至:
std::accumulate(...)(SUM >> q_sum)(GCD >> q_gcd)(MAX >> q_max);
其次,添加一个重载,该重载返回累加单个量的函数的第一个结果:
scalar_t result() const
{
// MXT_ASSERT(O2 == op_void);
return result(op_t<O1>());
}
// now .result(SUM) is equivalent to .result()
int S = std::accumulate(data, data+7, collect(...)*SUM).result();
8.5.驱动程序
一个写得好的算法避免了不必要的代码乘法。要重写一个现有的算法以获得更大的通用性,您必须从其中删除一些“固定的”逻辑,并通过一个模板参数(通常是一个仿函数)再次插入它:
template <typename iterator_t>
void sort(iterator_t begin, iterator_t end)
{
for (...)
{
// ...
if (a<b) // operator< is a good candidate for becoming a functor
{}
}
}
所以你把它改写成:
template <typename iterator_t, typename less_t>
void sort(iterator_t begin, iterator_t end, less_t less)
{
for (...)
{
// now we ask the functor to "plug" its code in the algorithm
if (less(a,b))
{}
}
}
一个驱动是一个可以一路引导算法的对象。
函子和驱动的主要区别在于前者有一个通用的类似函数的接口(至少是 operator()),这个接口是开放给用户定制的。另一方面,驱动程序是一个具有详细接口的低级对象,它不应该被定制(除了它的名字,它甚至可能没有被记录,就像它是一个标记类型)。框架本身将提供一个小的固定驱动程序集。
考虑下面的例子。您需要一个 sq 函数,它可以选择在 std::cerr 上记录结果。因为如果您接收一个通用的 logger 对象,您就不能强制这样的约束,所以您切换到驱动程序,然后提供一些:
struct dont_log_at_all
{
bool may_I_log() const { return false; }
};
struct log_everything
{
bool may_I_log() const { return true; }
};
struct log_ask_once
{
bool may_I_log() const
{
static bool RESULT = AskUsingMessageBox("Should I log?", MSG_YN);
return RESULT;
}
};
template <typename scalar_t, typename driver_t>
inline scalar_t sq(const scalar_t& x, driver_t driver)
{
const scalar_t result = (x*x);
if (driver.may_I_log())
std::cerr << result << std::endl;
return result;
}
template <typename scalar_t>
inline scalar_t sq(const scalar_t& x)
{
return sq(x, dont_log_at_all());
}
注意,driver_t::may_I_log()既不包含关于平方的代码,也不包含关于日志记录的代码。它只是做出一个决定,驱动算法的流程。
驱动程序的最大优势是减少调试时间,因为主算法是一个单一的函数。通常驱动程序对运行时的影响很小。然而,没有什么可以阻止一个驱动程序执行长时间复杂的计算。
通常,您总是通过实例调用驱动程序。一个界面,如
template <typename driver_t>
void explore(maze_t& maze, driver_t driver)
{
while (!driver.may_I_stop())
{ ... }
}
比它的无状态对应物 更一般 14 :
template <typename driver_t>
void explore(maze_t& maze)
{
while (driver_t::may_I_stop())
{ ... }
}
驱动程序在某种程度上类似于“公共非虚拟/受保护虚拟”的经典 C++ 习语(见 6.3 节)。关键的相似之处在于算法的结构是固定的。希望用户只定制特定的部分,这些部分只在基础设施需要时运行。 十五
8.6.阿尔戈斯
一个 algor ,或者算法函子,是一个嵌入算法的对象,或者简单地说是一个带状态的算法。
标准 C++ 库提供了一个头,它只包含函数。因此,确定函数和算法是很自然的事情,但不一定是这样。
algor 对象实现了一个简单的类似函数的接口—通常是 operator()—用于算法的执行,但是它的状态允许更快的重复执行。
algor 有用的最简单的情况是缓冲内存分配。std::stable_sort 可能需要分配一个临时缓冲区,这个缓冲区在函数返回时必须释放。通常这不是一个问题,因为花费在(单个)内存分配上的时间是由算法本身的执行决定的。一个小的输入将导致一个小的内存请求,这是“快速的”(操作系统倾向于支持小的分配)。大量的输入将导致“缓慢”的内存请求,但这一额外的时间将被忽略,因为算法将需要更多的时间来运行。
然而,在有些情况下,单个缓冲区足以满足许多请求。对许多长度相似的向量进行稳定排序时,如果在对象中维护缓冲区,可以节省分配/释放时间:
template <typename T>
class stable_sort_algor
{
buffer_type buffer_;
public:
template <RandomAccessIterator>
void operator()(RandomAccessIterator begin, RandomAccessIterator end)
{
// ensure that buffer_ is large enough
// if not, reallocate
// then perform the stable sort
}
~stable_sort_algor()
{
// release buffer_
}
};
综上所述,最简单的 algor 只是一种带状态的函子(在最后一种情况下,是一个临时缓冲区),但 algor 可能有更丰富的接口,超越了函子。
一般来说,算法不会被复制或赋值。它们被构造和重用(比如在一个循环中)或者在一次执行中用作未命名的临时变量。因此,你不需要担心效率,只需要担心安全。如果 buffer_type 不能被安全复制(如果是指针的话),就显式禁用所有危险的成员函数,使它们成为私有或公共的无操作。如果 buffer_type
另一种有用的 algor 是一个 self- 累加器 ,它可以一次保存多个结果。不涉及缓冲(见第 8.4 节)。
template <typename T>
class accumulator
{
T max_;
T min_;
T sum_;
// ...
public:
accumulator()
: sum_(0) // ...
{
}
template <typename iterator_t>
accumulator<T>& operator()(iterator_t begin, iterator_t end)
{
for (;begin != end; ++begin)
{
sum_ += *begin;
// ...
}
return *this;
}
T max() const { return max_; }
T min() const { return min_; }
T sum() const { return sum_; }
// ...
};
int main()
{
double data[] = {3,4,5 };
// single invocation
double SUM = accumulator<double>()(data, data+3).sum();
// multiple results are needed
accumulator<double> A;
A(data, data+3);
std::cout << "Range: " << A.max()-A.min();
}
一个交互式算法 有一个允许呼叫者一步一步运行算法的界面。例如,假设您必须计算平方根,达到一定的精度:
template <typename scalar_t>
class interactive_square_root
{
scalar_t x_;
scalar_t y_;
scalar_t error_;
public:
interactive_square_root(scalar_t x)
: x_(x)
{
iterate();
}
void iterate()
{
// precondition:
// y_ is some kind of approximate solution for y2=x
// error_ is |y2-x|
// now compute a better approximation
}
scalar_t error() const
{
return error_;
}
operator scalar_t() const
{
return y_;
}
};
驱动算法的是用户:
int main()
{
interactive_square_root<double> ISR(3.14);
while (ISR.error()>0.00001)
{
ISR.iterate();
}
double result = ISR;
}
这种算法通常从构造函数中获取所有参数。
一个常见的用例是产生一组解决方案的算法。执行后,成员函数允许用户以某种顺序“访问”所有解。 16 这些算法可能会做构造函数中的所有工作:
template <typename string_t>
class search_a_substring
{
const string_t& text_;
std::vector<size_t> position_;
public:
search_a_substring(const string_t& TEXT, const string_t& PATTERN)
: text_(TEXT)
{
// search immediately every occurrence of PATTERN in TEXT
// store all the positions in position_
}
bool no_match() const { return position_.empty(); }
// the simplest visitation technique
// is... exposing iterators
typedef std::vector<size_t>::const_iterator position_iterator;
position_iterator begin() const
{
return position_.begin();
}
position_iterator end() const
{
return position_.end();
}
};
在子串匹配的情况下,迭代器可能会从第一个匹配到最后一个匹配。在数值最小化问题中,解可能是 N 个点,在这些点上函数具有目前为止找到的最小值。
一个更复杂的访问者接受接口可以接受两个输出 迭代器,algor 将在其中编写它的解决方案。您可以根据迭代器 value_type 在解决方案上构建一个“自定义视图”。例如,内部计算对(Xj,Yj)的 algor 可能只发出第一个分量或整个对(下面是一个简化的例子):
class numerical_minimizer
{
std::function<double (double)> F;
std::vector<double> X_; // all the points where F has minima
public:
// ...
template <typename out_t>
out_t visit(out_t beg, out_t end) const
{
typedef typename std::iterator_traits<out_t>::value_type> val_t;
int i=0;
while (beg != end)
*beg++ = build_result(i++, instance_of<val_t>());
return beg;
}
private:
template <typename T>
double build_result(int i, instance_of<T>) const
{
return X_[i];
}
using std::pair;
template <typename T>
pair<double,double> build_result(int i, instance_of<pair<T,T>>) const
{
return std::make_pair(X_[i], F(X_[i]));
}
};
8.7.转发和引用包装器
对于一个类模板来说,保存一个泛型类型的成员是一种常见的习惯用法,类会将执行调度给这个成员。
template <typename T>
class test
{
T functor_;
public:
typename T::value_type operator()(double x) const
{
return functor_(x); // call forward
}
};
由于成员的确切类型未知,您可能必须实现 test::operator()的几个重载。因为这是一个模板,所以这不是一个问题,因为实际需要的将被实例化,其余的将被忽略。
template <typename T>
class test
{
T functor_;
public:
/* we don't know how many arguments functor_ needs */
template <typename T1>
typename T::value_type operator()(T1 x) const
{
return functor_(x); // call forwarding
}
template <typename T1, typename T2>
typename T::value_type operator()(T1 x, T2 y) const
{
return functor_(x, y); // call forwarding
}
// more...
};
调用错误的重载(即提供太多或不受支持的参数)将导致编译器错误。但是,请注意,参数是通过值转发的,因此您可以修改原型:
template <typename T1>
typename T::value_type operator()(const T1& x) const
{
return functor_(x); // call forwarding
}
但是如果 T 需要一个非常数引用的参数,代码就不会编译。
为了理解问题的严重性,考虑一个稍微不同的例子,其中您用未指定数量的参数构造了一个成员。
STL 指南建议为类测试编写一个单独的构造函数,它(可能)接受一个先前构造的 T 类型的对象:
test(const T& data = T())
: member_(data)
{
}
这种策略并不总是可行的。特别是,T 可能有一个不可访问的复制构造函数,或者它可能是非常量引用。
事实上,让我们暂时忘记 STL 风格,采用与前面相同的 operator()习惯用法。
template <typename T>
class bad_test
{
T member_;
public:
template <typename X1>
bad_test(X1 arg1)
: member_(arg1)
{
}
template <typename X1, typename X2>
bad_test(X1 arg1, X2 arg2)
: member_(arg1, arg2)
{
}
};
正如所写的,bad_test 编译成功,但是一个微妙的 bug 出现了 17 :
int main(int argc, char* argv[])
{
double x = 3.14;
bad_test<double&> urgh(x); // unfortunately, it compiles
urgh.member_ = 6.28; // bang!
int i = 0;
assert(x == 6.28); // assertion failed!
// ...
}
urgh 的构造函数是在 double 类型上实例化的,而不是 double&,所以 urgh.member_ 是指其构造函数堆栈中的一个临时位置(即 arg1 占用的存储空间),其内容是 x 的临时副本。
因此,您修改 bad_test 来通过常量引用转发参数。至少 good_test 不会编译(const double&不能转换成 double&)。
template <typename T>
class good_test
{
T member_;
public:
template <typename X1>
good_test(const X1& arg1)
: member_(arg1)
{
}
};
然而,额外的包裹层可以解决这两个问题:
template <typename T>
class reference_wrapper
{
T& ref_;
public:
explicit reference_wrapper(T& r)
: ref_(r)
{
}
operator T& () const
{
return ref_;
}
T* operator& () const
{
return &ref_;
}
};
template <typename T>
inline reference_wrapper<T> by_ref(T& x)
{
return reference_wrapper<T>(x);
}
int main()
{
double x = 3.14;
good_test<double> y0(x); // ok: x is copied into y0.member_
good_test<double&> y1(x); // compiler error!
y1.member_ = 6.28; // would be dangerous, but does not compile
good_test<double&> y2(by_ref(x));
y2.member_ = 6.28; // ok, now x == 6.28
}
使用 by_ref,good_test 构造函数在参数 const reference_wrapper 上被实例化,然后被转换为 double&。
注同样,参数转发问题在 C++0x 中用 R 值引用解决了。
1 它们被称为“透明的”。据作者所知,这本书是这个想法第一次公开出现的地方。详见www . open-STD . org/JT C1/sc22/wg21/docs/papers/2012/n 3421 . htm。
2 由于大多数函子是无状态的,因此不受初始化问题的影响,全局常量可以在头文件中创建。
作为一个规则,为了表达,最好写一个完全合格的人::年龄而不仅仅是年龄,所以你必须假设 Person 里面有一个 age_t 类型的静态常量。这也允许 age_t 成为人的朋友。还可以考虑 Person::AGE(),其中 AGE 要么是 age_t 的成员 typedef,要么是返回 age_t 的默认实例的静态成员函数。
4 详细描述见 4.2.1 节。
5 细心的读者会注意到,下面的例子确实传递了数组的长度,即使它总是被忽略。
6 类似的 STL 结构反而仅仅是在函子中嵌入一个指针。
7 有些编译器,包括 VC,不会注意到区别;但是,海合会确实在意。
8 本段仅作为教学示例,不作为生产代码的解决方案。在实践中,这个问题可以通过提升编译器对错误的警告,或者通过使用现代 C++ 强类型枚举来解决。然而,作为一个(元)程序员如何变通 C++ 语法来解决小问题的例子,这是很有启发性的。
9 如果 X 有类型 const A,wrap 会推导出 T=const A 并挑选第二个重载,前提是你仔细实现 enum_var_helper。
然而,有一些在线累加器可以很准确地估计个百分点。
11 你可能想再回头看看第五章。
12 例如,maxmin 算法的复杂度比分两步计算 max 和 min 低 25%。
13 关于这一点详见第 9.3 节**。**
**14 特质在某种程度上等同于无国籍司机。
15 参见 www.gotw.ca/publication…
16 这和 std::regex 的工作方式有些相似。
17 这个例子写得好像所有成员都是公共的。**