我有一个我正在尝试配置其行为的类。
template<int ModeT, bool IsAsync, bool IsReentrant> ServerTraits;
然后稍后我有我的服务器对象本身:
template<typename TraitsT>
class Server {…};
我的问题是我上面的用法是我的命名错误吗?我的模板化参数实际上是策略而不是特征吗?
什么时候模板化参数是一个特征而不是一个策略?
我有一个我正在尝试配置其行为的类。
template<int ModeT, bool IsAsync, bool IsReentrant> ServerTraits;
然后稍后我有我的服务器对象本身:
template<typename TraitsT>
class Server {…};
我的问题是我上面的用法是我的命名错误吗?我的模板化参数实际上是策略而不是特征吗?
什么时候模板化参数是一个特征而不是一个策略?
策略是将行为注入父类的类(或类模板),通常通过继承。通过将父接口分解为正交(独立)维度,策略类形成了更复杂接口的构建块。一种常见的模式是将策略作为用户可定义的模板(或模板模板)参数提供库提供的默认值。标准库中的一个示例是分配器,它们是所有 STL 容器的策略模板参数
template<class T, class Allocator = std::allocator<T>> class vector;
在这里,Allocator
模板参数(它本身也是一个类模板!)将内存分配和释放策略注入到父类std::vector
中。如果用户不提供分配器,std::allocator<T>
则使用默认值。
与基于模板的多态性一样,策略类的接口要求是隐式的和语义的(基于有效的表达式),而不是显式的和句法的(基于虚拟成员函数的定义)。
请注意,最近的无序关联容器具有多个策略。除了通常的Allocator
模板参数外,它们还采用Hash
默认为std::hash<Key>
函数对象的策略。这允许无序容器的用户沿多个正交维度(内存分配和散列)配置它们。
特征是从泛型类型中提取属性的类模板。有两种特征:单值特征和多值特征。单值特征的示例是标题中的那些<type_traits>
template< class T >
struct is_integral
{
static const bool value /* = true if T is integral, false otherwise */;
typedef std::integral_constant<bool, value> type;
};
单值特征通常用于模板元编程和 SFINAE 技巧中,以根据类型条件重载函数模板。
多值特征的示例分别是来自 headers<iterator>
和的 iterator_traits 和 allocator_traits <memory>
。由于特征是类模板,它们可以被专门化。下面是一个 for 特化的iterator_traits
例子T*
template<T>
struct iterator_traits<T*>
{
using difference_type = std::ptrdiff_t;
using value_type = T;
using pointer = T*;
using reference = T&;
using iterator_category = std::random_access_iterator_tag;
};
该表达式std::iterator_traits<T>::value_type
可以使成熟的迭代器类的通用代码甚至可用于原始指针(因为原始指针没有 member value_type
)。
在编写您自己的通用库时,考虑用户可以专门化您自己的类模板的方式很重要。但是,必须小心,不要让用户通过使用特征的特化来注入而不是提取行为而成为单一定义规则的受害者。套用安德烈亚历山德雷斯库的这篇旧帖子
根本的问题是,没有看到特性的特殊版本的代码仍然可以编译,可能会链接,有时甚至可能会运行。这是因为在没有显式特化的情况下,非特化模板会发挥作用,可能会实现适用于您的特殊情况的通用行为。因此,如果不是应用程序中的所有代码都看到相同的特征定义,则违反了 ODR。
C++11std::allocator_traits
通过强制所有 STL 容器只能Allocator
通过std::allocator_traits<Allocator>
. 如果用户选择不提供或忘记提供一些必需的策略成员,则特征类可以介入并为那些缺失的成员提供默认值。因为allocator_traits
它本身不能被专门化,所以用户总是必须通过一个完全定义的分配器策略来定制他们的容器内存分配,并且不会发生静默的 ODR 违规。
请注意,作为库编写者,仍然可以特化特征类模板(就像 STLiterator_traits<T*>
在正如 STL 在allocator_traits<A>
) 中所做的那样。
更新:特征类的用户定义特化的 ODR 问题主要发生在特征被用作全局类模板并且您不能保证所有未来的用户都会看到所有其他用户定义的特化时。策略是本地模板参数并包含所有相关定义,允许用户定义它们而不会干扰其他代码。仅包含类型和常量(但不包含行为功能)的本地模板参数可能仍被称为“特征”,但它们对其他代码(如std::iterator_traits
and )不可见std::allocator_traits
。
我想你会在Andrei Alexandrescu 的这本书中找到你的问题的最佳答案。在这里,我将尝试给出一个简短的概述。希望它会有所帮助。
特征类是通常旨在将类型与其他类型或常量值相关联以提供这些类型的特征的元函数的类。换句话说,它是一种对 types 的属性进行建模的方法。该机制通常利用模板和模板特化来定义关联:
template<typename T>
struct my_trait
{
typedef T& reference_type;
static const bool isReference = false;
// ... (possibly more properties here)
};
template<>
struct my_trait<T&>
{
typedef T& reference_type;
static const bool isReference = true;
// ... (possibly more properties here)
};
上面的特征元函数my_trait<>
将引用类型T&
和常量布尔值关联false
到所有T
本身不是引用的类型;另一方面,它将引用类型T&
和常量布尔值true
与所有引用T
类型相关联。
例如:
int -> reference_type = int&
isReference = false
int& -> reference_type = int&
isReference = true
在代码中,我们可以如下断言上述内容(下面的所有四行都将编译,这意味着满足第一个参数中表达的条件static_assert()
):
static_assert(!(my_trait<int>::isReference), "Error!");
static_assert( my_trait<int&>::isReference, "Error!");
static_assert(
std::is_same<typename my_trait<int>::reference_type, int&>::value,
"Error!"
);
static_assert(
std::is_same<typename my_trait<int&>::reference_type, int&>::value,
"Err!"
);
在这里你可以看到我使用了标准std::is_same<>
模板,它本身就是一个接受两个而不是一个类型参数的元函数。事情可以在这里变得任意复杂。
尽管std::is_same<>
是type_traits
标题的一部分,但有些人认为只有当它充当元谓词(因此,接受一个模板参数)时,类模板才是类型特征类。然而,据我所知,术语并没有明确定义。
有关在 C++ 标准库中使用特征类的示例,请查看输入/输出库和字符串库的设计方式。
政策略有不同(实际上,完全不同)。它通常是一个类,它指定另一个通用类的行为应该与某些可能以几种不同方式实现的操作有关(因此,其实现由策略类决定)。
例如,一个通用的智能指针类可以设计为一个模板类,它接受一个策略作为模板参数来决定如何处理引用计数——这只是一个假设的、过于简单化和说明性的例子,所以请尝试抽象从这个具体的代码和机制上看。
这将允许智能指针的设计者不对引用计数器的修改是否应以线程安全的方式进行硬编码承诺:
template<typename T, typename P>
class smart_ptr : protected P
{
public:
// ...
smart_ptr(smart_ptr const& sp)
:
p(sp.p),
refcount(sp.refcount)
{
P::add_ref(refcount);
}
// ...
private:
T* p;
int* refcount;
};
在多线程上下文中,客户端可以使用智能指针模板的实例化和实现引用计数器的线程安全递增和递减的策略(此处假定 Windows 平台):
class mt_refcount_policy
{
protected:
add_ref(int* refcount) { ::InterlockedIncrement(refcount); }
release(int* refcount) { ::InterlockedDecrement(refcount); }
};
template<typename T>
using my_smart_ptr = smart_ptr<T, mt_refcount_policy>;
另一方面,在单线程环境中,客户端可以使用简单地增加和减少计数器值的策略类来实例化智能指针模板:
class st_refcount_policy
{
protected:
add_ref(int* refcount) { (*refcount)++; }
release(int* refcount) { (*refcount)--; }
};
template<typename T>
using my_smart_ptr = smart_ptr<T, st_refcount_policy>;
通过这种方式,库设计者提供了一个灵活的解决方案,能够在性能和安全性之间提供最佳折衷(“您无需为不使用的东西付费”)。
如果您使用 ModeT、IsReentrant 和 IsAsync 来控制服务器的行为,那么它就是一个策略。
或者,如果您想要一种将服务器的特征描述给另一个对象的方法,那么您可以定义一个特征类,如下所示:
template <typename ServerType>
class ServerTraits;
template<>
class ServerTraits<Server>
{
enum { ModeT = SomeNamespace::MODE_NORMAL };
static const bool IsReentrant = true;
static const bool IsAsync = true;
}
这里有几个例子来澄清亚历克斯·张伯伦的评论:
特征类的一个常见示例是 std::iterator_traits。假设我们有一个带有成员函数的模板类 C,它接受两个迭代器,迭代值,并以某种方式累积结果。我们希望积累策略也被定义为模板的一部分,但将使用策略而不是特征来实现这一目标。
template <typename Iterator, typename AccumulationPolicy>
class C{
void foo(Iterator begin, Iterator end){
AccumulationPolicy::Accumulator accumulator;
for(Iterator i = begin; i != end; ++i){
std::iterator_traits<Iterator>::value_type value = *i;
accumulator.add(value);
}
}
};
策略被传递给我们的模板类,而特征是从模板参数派生的。所以你所拥有的更像是一项政策。在某些情况下,特征更合适,政策更合适,并且通常可以使用任何一种方法实现相同的效果,从而引发关于哪种方法最具表现力的争论。