特质和政策有什么区别?

时间:2013-02-05 22:16:56

标签: c++ template-meta-programming typetraits policy-based-design

我有一个我试图配置其行为的类。

template<int ModeT, bool IsAsync, bool IsReentrant> ServerTraits;

然后,我有自己的服务器对象:

template<typename TraitsT>
class Server {...};

我的问题是我上面的用法是我的命名错误名称?我的模板化参数实际上是一个策略而不是特征吗?

模板化参数何时是特征与策略?

4 个答案:

答案 0 :(得分:82)

策略

策略是将注入行为的类(或类模板)添加到父类中,通常是通过继承。通过将父接口分解为正交(独立)维度,策略类构成了更复杂接口的构建块。常见的模式是将策略作为用户可定义的模板(或模板模板)参数提供,并提供库提供的默认值。标准库中的一个示例是Allocators,它是所有STL容器的策略模板参数

template<class T, class Allocator = std::allocator<T>> class vector;

这里,Allocator模板参数(它本身也是一个类模板!)将内存分配和释放策略注入父类std::vector。如果用户未提供分配器,则使用默认的std::allocator<T>

与基于模板的多态性一样,策略类的接口要求是隐式和语义(基于有效表达式)而不是显式和语法(基于虚拟成员函数的定义) 。

请注意,较新的无序关联容器具有多个策略。除了通常的Allocator模板参数外,它们还采用默认为Hash函数对象的std::hash<Key>策略。这允许无序容器的用户沿多个正交维度(内存分配和散列)配置它们。

性状

Traits是来自泛型类型的提取属性的类模板。有两种特征:单值特征和多值特征。单值特征的示例是标题<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技巧则根据类型条件重载函数模板。

多值特征的示例分别是标题<iterator><memory>中的iterator_traits和allocator_traits。由于traits是类模板,因此它们可以是专用的。下面是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使得为完全成熟的迭代器类创建通用代码成为可能,即使对于原始指针也是如此(因为原始指针没有成员value_type)。

政策和特征之间的相互作用

在编写自己的通用库时,重要的是要考虑用户可以专门化自己的类模板的方法。但是,必须要小心,不要让用户成为一个定义规则的受害者,通过使用特征的特征来注入而不是提取行为。用Andrei Alexandrescu的这句old post来解释

  

根本问题是没有看到专门的代码   特征的版本仍将编译,很可能链接,和   有时甚至可能会跑。这是因为在没有的情况下   明确的专业化,非专业模板可能会开始   实现适用于您的特殊情况的通用行为   好。因此,如果不是应用程序中的所有代码都看到了   对特征的定义相同,ODR被违反。

C ++ 11 std::allocator_traits通过强制所有STL容器只能通过Allocatorstd::allocator_traits<Allocator>策略中提取属性来避免这些陷阱。如果用户选择不提供或忘记提供某些必需的策略成员,则traits类可以介入并为缺少的成员提供默认值。由于allocator_traits本身不能专门化,因此用户必须始终传递完全定义的分配器策略,以便自定义其容器内存分配,并且不会发生静默ODR违规。

请注意,作为库编写者,仍然可以专门化traits类模板(如iterator_traits<T*>中的STL所做的那样),但优良作法是将所有用户定义的特化通过策略类传递给多值可以提取专门行为的特征(正如STL在allocator_traits<A>中所做的那样)。

更新:特征类的用户定义特化的ODR问题主要发生在将特征用作全局类模板时,并且您无法保证将来所有用户都会看到所有其他用户定义的专业化。策略是本地模板参数并包含所有相关定义,允许用户定义它们而不会干扰其他代码。仅包含类型和常量但不具有行为功能的本地模板参数可能仍称为“特征”,但对std::iterator_traitsstd::allocator_traits等其他代码不可见。

答案 1 :(得分:24)

我认为您会在 this book by Andrei Alexandrescu 中找到问题的最佳答案。在这里,我将尝试简要介绍一下。希望它会有所帮助。


traits class 是一个类,通常用于将类型与其他类型或常量值相关联的元函数,以提供这些类型的特征。换句话说,它是一种模拟属性属性的方法。该机制通常利用模板和模板专门化来定义关联:

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 ++标准库中使用traits类的示例,请查看输入/输出库和字符串库的设计方法。


政策略有不同(实际上,非常不同)。它通常是指一个类,它指定另一个泛型类的行为应该与某些可能以多种不同方式实现的操作相关(因此,其实现可以留给策略类)。

例如,可以将通用智能指针类设计为模板类,该模板类接受策略作为模板参数来决定如何处理引用计数 - 这只是一个假设的,过于简单化和说明性的示例,所以请尝试从这个具体的代码中抽象出来,并专注于机制

这将允许智能指针的设计者不做出关于参考计数器的修改是否应以线程安全方式进行的硬编码承诺:

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>;

通过这种方式,图书馆设计师提供了一种灵活的解决方案,能够在性能和安全性之间提供最佳折衷(“您不需要为不使用的内容付费”)。

答案 2 :(得分:3)

如果您使用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;
}

答案 3 :(得分:1)

以下是一些澄清Alex Chamberlain评论的例子:

特征类的一个常见示例是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);
        }
    }
};

策略传递给我们的模板类,而特征是从模板参数派生的。所以你拥有的更像是一个政策。在某些情况下,特征更为合适,而且政策更合适,并且通常可以通过任何一种方法实现相同的效果,从而引发关于哪种方式最具表现力的争论。