Boost C++ 库

...世界上最受尊敬和设计最精良的 C++ 库项目之一。 Herb SutterAndrei Alexandrescu, C++ 编码标准

PrevUpHomeNext

调用特性

简介
拷贝构造性
示例
原理
参考

<boost/call_traits.hpp> 的所有内容都在 namespace boost 内部定义。

模板类 call_traits<T> 封装了将类型为 T 的参数传递给函数或从函数传递出来的“最佳”方法,并包含如下表定义的 typedef 集合。call_traits 的目的是确保诸如 “引用的引用” 之类的问题永远不会发生,并且参数以最有效的方式传递,如 示例 所示。在每种情况下,如果您现有的实践是使用左侧定义的类型,则将其替换为右侧 call_traits 定义的类型。

请注意,对于不支持部分特化或成员模板的编译器,使用 call_traits 不会带来任何好处:在这种情况下,call_traits 定义的类型将始终与现有实践相同。此外,如果编译器仅支持成员模板而不支持部分模板特化(例如 Visual C++ 6),则 call_traits 不能用于数组类型,但它仍然可以用于解决引用的引用问题。

表 1.2. call_traits 类型

现有实践

call_traits 等效项

描述

注释

T

(按值返回)

call_traits<T>::value_type

定义表示类型 T 的“值”的类型。

用于按值返回的函数,或可能用于类型为 T 的存储值。

2

T&

(返回值)

call_traits<T>::reference

定义表示对类型 T 的引用的类型。

用于通常返回 T& 的函数。

1

const T&

(返回值)

call_traits<T>::const_reference

定义表示对类型 T 的常量引用的类型。

用于通常返回 const T& 的函数。

1

const T&

(函数参数)

call_traits<T>::param_type

定义表示将类型为 T 的参数传递给函数的“最佳”方式的类型。

1,3


注释

  1. 如果 T 已经是引用类型,则 call_traits 的定义方式确保 “引用的引用” 不会发生(需要部分特化)。
  2. 如果 T 是数组类型,则 call_traitsvalue_type 定义为“指向类型的常量指针”而不是“类型数组”(需要部分特化)。请注意,如果您将 value_type 用作存储值,则这将导致存储“指向数组的常量指针”而不是数组本身。这可能是好事也可能不是好事,具体取决于您实际需要什么(换句话说,请小心!)。
  3. 如果 T 是小型内置类型或指针,则 param_type 定义为 T const,而不是 T const&。如果函数体内的循环依赖于传递的参数,这可以提高编译器优化这些循环的能力,传递的参数的语义在其他方面保持不变(需要部分特化)。

下表定义了哪些 call_traits 类型始终可以从哪些其他类型进行拷贝构造

表 1.3. 哪些 call_traits 类型始终可以从哪些其他类型进行拷贝构造

T

value_type

reference

const_reference

param_type

T

当且仅当 T 是可拷贝构造的

当且仅当 T 是可拷贝构造的

value_type

当且仅当 T 是可拷贝构造的

当且仅当 T 是可拷贝构造的

reference

当且仅当 T 是可拷贝构造的

当且仅当 T 是可拷贝构造的

const_reference

当且仅当 T 是可拷贝构造的

param_type

当且仅当 T 是可拷贝构造的

当且仅当 T 是可拷贝构造的


如果 T 是可赋值类型,则以下赋值是可能的

表 1.4. 哪些 call_traits 类型可以从哪些其他类型赋值

T

value_type

reference

const_reference

param_type

T

-

-

-

value_type

-

-

-

reference

-

-

-

const_reference

-

-

-

param_type

-

-

-


下表显示了 call_traits 对各种类型的影响。

表 1.5. call_traits 类型的示例

call_traits::value_type

call_traits::reference

call_traits::const_reference

call_traits::param_type

适用于

my_class

my_class

my_class&

const my_class&

my_class const&

所有用户定义的类型

int

int

int&

const int&

int const

所有小型内置类型

int*

int*

int*&

int* const &

int* const

所有指针类型

int&

int&

int&

const int&

int&

所有引用类型

const int&

const int&

const int&

const int&

const int&

所有常量引用类型

int[3]

const int*

int(&)[3]

const int(&)[3]

const int* const

所有数组类型

const int[3]

const int*

const int(&)[3]

const int(&)[3]

const int* const

所有常量数组类型


该表假设编译器支持部分特化:如果不支持,则所有类型的行为方式都与“my_class”条目相同,并且 call_traits 不能用于引用或数组类型。

以下类是一个简单的类,它按值存储某种类型 T(请参阅 call_traits_test.cpp 文件)。目的是说明如何使用每个可用的 call_traits typedef

template <class T>
struct contained
{
   // define our typedefs first, arrays are stored by value
   // so value_type is not the same as result_type:
   typedef typename boost::call_traits<T>::param_type       param_type;
   typedef typename boost::call_traits<T>::reference        reference;
   typedef typename boost::call_traits<T>::const_reference  const_reference;
   typedef T                                                value_type;
   typedef typename boost::call_traits<T>::value_type       result_type;

   // stored value:
   value_type v_;

   // constructors:
   contained() {}
   contained(param_type p) : v_(p){}
   // return byval:
   result_type value() { return v_; }
   // return by_ref:
   reference get() { return v_; }
   const_reference const_get()const { return v_; }
   // pass value:
   void call(param_type p){}

};

考虑 std::binder1st 的定义

template <class Operation>
class binder1st :
   public std::unary_function<typename Operation::second_argument_type, typename Operation::result_type>
{
protected:
   Operation op;
   typename Operation::first_argument_type value;
public:
   binder1st(const Operation& x, const typename Operation::first_argument_type& y);
   typename Operation::result_type operator()(const typename Operation::second_argument_type& x) const;
};

现在考虑在 functor 将其第二个参数作为引用的相对常见的情况下会发生什么,这意味着 Operation::second_argument_type 是引用类型,operator() 现在最终会接受对引用的引用作为参数,而这在目前是不合法的。此处的解决方案是修改 operator() 以使用 call_traits

typename Operation::result_type operator()(typename call_traits<typename Operation::second_argument_type>::param_type x) const;

现在,在 Operation::second_argument_type 是引用类型的情况下,参数作为引用传递,并且不会发生“引用的引用”。

如果我们将数组的名称作为 std::make_pair 的一个(或两个)参数传递,则模板参数推导会将传递的参数推导为“指向 T 数组的常量引用”,这也适用于字符串字面量(实际上是数组字面量)。因此,它不是返回指针对,而是尝试返回数组对,并且由于数组类型不可拷贝构造,因此代码编译失败。一种解决方案是将 std::make_pair 的参数显式转换为指针,但 call_traits 提供了一种更好的自动解决方案,即使在泛型代码中也能安全地工作,而强制转换可能会做错事。

template <class T1, class T2>
std::pair<
   typename boost::call_traits<T1>::value_type,
   typename boost::call_traits<T2>::value_type>
      make_pair(const T1& t1, const T2& t2)
{
   return std::pair<
      typename boost::call_traits<T1>::value_type,
      typename boost::call_traits<T2>::value_type>(t1, t2);
}

在这里,如果推导的类型是数组,则推导的参数类型将自动降级为指针,标准绑定器和适配器中也会发生类似的情况:原则上,在任何“包装”临时对象的函数中,其类型都是推导出来的。请注意,std::make_pair 的函数参数不是用 call_traits 表示的:这样做会阻止模板参数推导发挥作用。

call_traits 模板将“优化”将小型内置类型作为函数参数的传递。当参数在循环体中使用时,这主要会产生影响。

在以下示例中(请参阅 fill_example.cpp),std::fill 的一个版本以两种方式进行了优化:如果传递的类型是单字节内置类型,则使用 std::memset 来实现填充,否则使用传统的 C++ 实现,但使用 call_traits “优化”传递的参数。

template <bool opt>
struct filler
{
   template <typename I, typename T>
   static void do_fill(I first, I last, typename boost::call_traits<T>::param_type val)
   {
      while(first != last)
      {
         *first = val;
         ++first;
      }
   }
};

template <>
struct filler<true>
{
   template <typename I, typename T>
   static void do_fill(I first, I last, T val)
   {
      std::memset(first, val, last-first);
   }
};

template <class I, class T>
inline void fill(I first, I last, const T& val)
{
   enum { can_opt = boost::is_pointer<I>::value
                   && boost::is_arithmetic<T>::value
                   && (sizeof(T) == 1) };
   typedef filler<can_opt> filler_t;
   filler_t::template do_fill<I,T>(first, last, val);
}

对于小型内置类型,这是“最佳”的原因是,当值作为 T const 而不是 const T& 传递时,编译器可以判断该值是常量并且没有别名。有了这些信息,编译器就能够将传递的值缓存在寄存器中,展开循环或使用显式并行指令:如果支持这些指令中的任何一个。您能从中获得多少好处取决于您的编译器 - 在这种情况下,我们确实需要一些准确的基准测试软件作为 boost 的一部分。

请注意,fill 的函数参数不是用 call_traits 表示的:这样做会阻止模板参数推导发挥作用。相反,fill 充当“瘦包装器”,用于执行模板参数推导,编译器将完全优化掉对 fill 的调用,并将其替换为对 filler<>::do_fill 的调用,后者确实使用了 call_traits

以下注释旨在简要描述 call_traits 中所做选择背后的原理。

所有用户定义的类型都遵循“现有实践”,无需评论。

小型内置类型(标准称为 基本类型)与现有实践的不同之处仅在于 param_type typedef。在这种情况下,传递 T const 与现有实践兼容,但在某些情况下可能会提高性能(请参阅 示例 4)。在任何情况下,这都不应比现有实践更差。

指针遵循与小型内置类型相同的原理。

对于引用类型,原理遵循 示例 2 - 不允许引用引用,因此 call_traits 成员的定义必须确保这些问题不会发生。有一个提案要修改语言,使“引用的引用是引用”(问题 #106,由 Bjarne Stroustrup 提交)。call_traits<T>::value_typecall_traits<T>::param_type 都提供了与该提案相同的效果,而无需更改语言。换句话说,这是一种解决方法。

对于数组类型,将数组作为参数的函数会将数组类型降级为指针类型:这意味着实际参数的类型与其声明的类型不同,这可能会在依赖于参数声明类型的模板代码中引起无穷无尽的问题。

例如

template <class T>
struct A
{
   void foo(T t);
};

在这种情况下,如果我们实例化 A<int[2]>,则传递给成员函数 foo 的参数的声明类型为 int[2],但其实际类型为 const int*。如果我们尝试在函数体中使用类型 T,则我们的代码很可能无法编译。

template <class T>
void A<T>::foo(T t)
{
   T dup(t); // doesn't compile for case that T is an array.
}

通过使用 call_traits,从数组到指针的降级是显式的,并且参数的类型与其声明的类型相同。

template <class T>
struct A
{
   void foo(typename call_traits<T>::value_type t);
};

template <class T>
void A<T>::foo(typename call_traits<T>::value_type t)
{
   typename call_traits<T>::value_type dup(t); // OK even if T is an array type.
}

对于 value_type(按值返回),同样只能返回指针,而不能返回整个数组的副本,并且 call_traits 使降级变得显式。value_type 成员在必须将数组显式降级为指针时非常有用 - 示例 3 提供了测试用例。

脚注:call_traits 的数组特化是所有 call_traits 特化中最不为人所理解的。如果给定的语义给您带来特定问题,或者没有解决特定的数组相关问题,那么我很乐意听到您的反馈。但是,大多数人可能永远不需要使用此特化。

参考

namespace boost {
  template<typename T> struct call_traits;

  template<typename T, std::size_t N> struct call_traits<const T[N]>;
  template<typename T> struct call_traits<T &>;
  template<typename T, std::size_t N> struct call_traits<T[N]>;
}

PrevUpHomeNext