特质和策略有什么区别

What is the difference between a trait and a policy?

本文关键字:什么 区别 策略      更新时间:2023-10-16

我有一个类,我正在尝试配置其行为。

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模板参数外,它们还采用默认为std::hash<Key>函数对象的Hash策略。这允许无序容器的用户沿多个正交维度(内存分配和哈希(配置它们。

性状

特征是从泛型类型中提取属性的类模板。有两种特征:单值特征和多值特征。单值特征的示例来自标头<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。由于特征是类模板,因此它们可以专门化。下面是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(。

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

在编写自己的泛型库时,重要的是要考虑用户如何专门化您自己的类模板。但是,必须小心,不要让用户通过使用特征的特殊化来注入而不是提取行为而成为"一个定义规则"的受害者。套用安德烈·亚历山德雷斯库的这篇旧文章

根本问题是代码看不到专门的 特征的版本仍将编译,可能会链接,并且 有时甚至可能会运行。这是因为在没有 显式专用化,非专用模板启动,可能 实现适用于您的特殊情况的通用行为,如 井。因此,如果不是应用程序中的所有代码都看到 与特征的定义相同,违反了 ODR。

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

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

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

我想你会在安德烈·亚历山德雷斯库的这本书中找到你问题的最佳答案。在这里,我将尝试做一个简短的概述。希望它会有所帮助。


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

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 标头的一部分,但有些人认为类模板只有在充当元谓词(因此接受一个模板参数(时才是类型 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 来控制服务器的行为,那么这是一个策略。

或者,如果你想要一种方法来向另一个对象描述服务器的特征,那么你可以定义一个traits类,如下所示:

template <typename ServerType>
class ServerTraits;
template<>
class ServerTraits<Server>
{
    enum { ModeT = SomeNamespace::MODE_NORMAL };
    static const bool IsReentrant = true;
    static const bool IsAsync = true;
}

这里有几个例子来澄清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);
        }
    }
};

策略被传递给我们的模板类,而特征是从模板参数派生的。所以你所拥有的更类似于一项政策。在某些情况下,特质更合适,政策更合适,通常任何一种方法都可以达到相同的效果,从而导致关于哪种方法最有表现力的争论。

策略由用户通过 API 传递,以主动选择在某个接口中应遵循的代码路径。

另一方面,库作者使用特征根据用户传递到 API 的内容选择某些重载。

特征是库

作者对用户输入做出反应的一种方式,而策略是用户积极影响库行为的一种方式。