c++ 特性和策略之间的区别是什么?

iovurdzv  于 2023-01-28  发布在  其他
关注(0)|答案(5)|浏览(150)

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

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

然后我有了服务器对象本身:

template<typename TraitsT>
class Server {…};

我的问题是我上面的用法是我的命名错误吗?我的模板化参数实际上是一个策略而不是一个trait吗?
什么时候模板化的论点是一种特性还是一种策略?

wgmfuz8q

wgmfuz8q1#

政策

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

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

这里,Allocator模板参数(它本身也是一个类模板!)将内存分配和释放策略注入到父类std::vector中,如果用户没有提供分配器,则使用默认的std::allocator<T>
在基于模板的多态性中,对策略类的接口要求是隐式的和语义的(基于有效表达式),而不是显式的和语法的(基于虚拟成员函数的定义)。
注意,最近的无序关联容器有不止一个策略。除了通常的Allocator模板参数,它们还采用Hash策略,默认为std::hash<Key>函数对象。这允许无序容器的用户沿着多个正交维度(内存分配和哈希)配置它们。

特征

traits是类模板,用来从泛型类型中**提取属性。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;
};

单值traits通常用于template-metaprogramming和SFINAE技巧中,以基于类型条件重载函数模板。
多值traits的例子是分别来自头文件<iterator><memory>的iterator_traits和allocator_traits。由于traits是类模板,所以它们可以被专门化。下面是iterator_traitsT*专门化的例子

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)。

策略和特性之间的交互

在编写自己的泛型库时,考虑用户可以专门化自己的类模板的方式是很重要的。但是,必须小心,不要让用户成为一个定义规则的牺牲品,不要使用特性的专门化来注入而不是提取行为。
根本的问题是,没有看到trait的特殊化版本的代码仍然可以编译,很可能链接,有时甚至可能运行。这是因为在没有显式特殊化的情况下,非特殊化模板会起作用,很可能实现一个通用行为,也适用于您的特殊情况。因此,如果应用程序中的所有代码看到的trait定义不同,违反了网上解决。
C++11 std::allocator_traits通过强制所有STL容器只能通过std::allocator_traits<Allocator>从它们的Allocator策略中提取属性来避免这些陷阱。如果用户选择不提供或忘记提供一些必需的策略成员,traits类可以介入并为那些缺失的成员提供默认值。因为allocator_traits本身不能被专门化,用户总是必须传递一个完全定义的分配器策略,以便定制他们的容器内存分配,并且不会发生静默ODR违规。
注意,作为库编写者,仍然可以专门化traits类模板(如iterator_traits<T*>中的STL),但是通过策略类将所有用户定义的专门化传递到可以提取专门化行为的多值traits中(如allocator_traits<A>中的STL)是一个很好的实践。

    • 更新**:traits类的用户定义专门化的ODR问题主要发生在traits被用作全局类模板时,并且您不能保证所有未来的用户将看到所有其他用户定义的专门化。策略是本地模板参数,包含所有相关的定义。允许用户定义它们而不干扰其他代码。只包含类型和常量-但不包含行为函数-的局部模板参数可能仍被称为"traits"但是它们对于像std::iterator_traitsstd::allocator_traits这样的其他代码是不可见的。
bqucvtff

bqucvtff2#

我想你会在**this book by Andrei Alexandrescu**中找到你的问题的最佳答案。在这里,我将尝试给予一个简短的概述。希望它会有所帮助。

  • *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

在代码中,我们可以如下Assert(下面的四行代码都将编译,这意味着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头的一部分,但是一些人认为类模板只有在充当元 predicate 时才是类型特征类(因此,接受 one template参数),然而,据我所知,术语没有明确定义。
关于traits类在C++标准库中的用法示例,请看一下输入/输出库和字符串库是如何设计的。

policy是稍微不同的东西(实际上,非常不同),它通常是一个类,指定另一个泛型类在某些操作方面应该有什么行为,这些操作可以通过几种不同的方式实现(因此,其实现由policy类决定)。

例如,一个通用的智能指针类可以被设计成一个模板类,它接受一个策略作为模板参数来决定如何处理引用计数--这只是一个假设的、过于简单化的示例,所以请尝试从这个具体的代码中抽象出来,并专注于 * 机制 *。
这将允许智能指针的设计者不做出关于引用计数器的修改是否应当以线程安全的方式完成的硬编码承诺:

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

通过这种方式,库设计人员提供了一种灵活的解决方案,能够在性能和安全性之间提供最佳折衷(“您不用为您不使用的东西付费”)。

lfapxunr

lfapxunr3#

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

p5fdfcr14#

这里有几个例子来澄清亚历克斯·张伯伦的评论:
trait类的一个常见例子是std::iterator_traits。假设我们有一个模板类C,它有一个成员函数,该函数接受两个迭代器,迭代值,并以某种方式累积结果。我们希望累积策略也被定义为模板的一部分,但将使用策略而不是trait来实现这一点。

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

策略被传递给我们的模板类,而trait则是从模板参数中派生出来的。所以你所拥有的更像是一个策略。在有些情况下,trait更合适,而策略更合适,通常两种方法都能达到同样的效果,这导致了关于哪种方法最具表达力的争论。

t98cgbkg

t98cgbkg5#

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

另一方面,Traits被库作者用来根据用户传递给API的内容选择某些重载。
特性是图书馆作者对用户输入做出React的一种方式,而策略是用户主动影响图书馆行为的一种方式。

相关问题