Skip to content

Latest commit

 

History

History
904 lines (654 loc) · 36.2 KB

File metadata and controls

904 lines (654 loc) · 36.2 KB

四、编译时技巧

在本章中,我们将介绍以下内容:

  • 编译时检查大小
  • 启用积分类型的函数模板使用
  • 禁用真实类型的函数模板使用
  • 从数字创建类型
  • 实现类型特征
  • 为模板参数选择最佳运算符
  • 在 C++ 03 中获取表达式类型

介绍

在本章中,我们将看到一些基本的例子,介绍如何在编译时检查、优化算法以及其他元编程任务中使用 Boost 库。

有些读者可能会问,*“我们为什么要关心编译时的事情?”*那是因为发布的版本程序编译一次,运行多次。我们在编译时做的越多,留给运行时的工作就越少,从而产生更快、更可靠的程序。只有当带有检查的部分代码被执行时,运行时检查才会被执行。编译时检查会阻止程序编译,最好是有意义的编译器错误消息。

这一章可能是最重要的一章。没有它,理解 Boost 源和其他类似 Boost 的库是不可能的。

编译时检查大小

假设我们正在编写一些序列化函数,将值存储在指定大小的缓冲区中:

#include <cstring> 
#include <boost/array.hpp> 

// C++ 17 has std::byte out of the box!
// Unfortunately this is as C++ 03 example. 
typedef unsigned char byte_t;

template <class T, std::size_t BufSizeV> 
void serialize_bad(const T& value, boost::array<byte_t, BufSizeV>& buffer) { 
    // TODO: check buffer size.
    std::memcpy(&buffer[0], &value, sizeof(value)); 
}

该代码存在以下问题:

  • 没有检查缓冲区的大小,因此它可能会溢出
  • 该功能可用于不可复制的类型,这会导致不正确的行为

我们可以通过添加一些断言来部分修复它,例如:

template <class T, std::size_t BufSizeV> 
void serialize_bad(const T& value, boost::array<byte_t, BufSizeV>& buffer) {  
    // TODO: think of something better.
    assert(BufSizeV >= sizeof(value));
    std::memcpy(&buffer[0], &value, sizeof(value)); 
}

但是,这是一个糟糕的解决方案。如果没有调用函数,运行时检查不会在调试模式下的测试期间触发断言。运行时检查甚至可以在发布模式下进行优化,因此可能会发生非常糟糕的事情。 BufSizeVsizeof(value)值在编译时是已知的。这意味着,如果缓冲区太小,我们可以强制这段代码编译失败,而不用运行时断言。

准备好

这个食谱需要一些 C++ 模板和Boost.Array库的知识。

怎么做...

让我们使用Boost.StaticAssertBoost.TypeTraits库来纠正解决方案。以下是如何:

#include <boost/static_assert.hpp> 
#include <boost/type_traits/has_trivial_copy.hpp> 

template <class T, std::size_t BufSizeV> 
void serialize(const T& value, boost::array<byte_t, BufSizeV>& buffer) { 
    BOOST_STATIC_ASSERT(BufSizeV >= sizeof(value)); 
    BOOST_STATIC_ASSERT(boost::has_trivial_copy<T>::value); 

    std::memcpy(&buffer[0], &value, sizeof(value)); 
}

它是如何工作的...

只有在编译时可以计算断言表达式并且可以隐式转换为bool时,才能使用BOOST_STATIC_ASSERT宏。这意味着您只能在BOOST_STATIC_ASSERT中使用sizeof()、静态常量、常量表达式变量、带有编译时已知参数的常量表达式函数以及其他常量表达式。如果断言表达式计算为false,则BOOST_STATIC_ASSERT将停止编译。在serialize函数的情况下,如果第一次静态断言失败,这意味着用户误用了serialize函数,并提供了非常小的缓冲区。

下面是更多的例子:

BOOST_STATIC_ASSERT(3 >= 1); 

struct some_struct { enum enum_t { value = 1}; }; 
BOOST_STATIC_ASSERT(some_struct::value); 

template <class T1, class T2> 
struct some_templated_struct 
{ 
    enum enum_t { value = (sizeof(T1) == sizeof(T2))}; 
}; 
BOOST_STATIC_ASSERT((some_templated_struct<int, unsigned int>::value));

template <class T1, class T2>
struct some_template { 
    BOOST_STATIC_ASSERT(sizeof(T1) == sizeof(T2));
};

If the BOOST_STATIC_ASSERT macro's assert expression has a comma sign in it, we must wrap the whole expression in additional brackets.

最后一个例子非常接近我们在serialize()函数第二行看到的。所以现在,是时候去发现更多关于Boost.TypeTraits图书馆的事情了。这个库提供了大量的编译时元函数,允许我们获取关于类型的信息和修改类型。元功能用法看起来像boost::function_name<parameters>::valueboost::function_name<parameters>::type。仅当T是简单的可复制类型时,boost::has_trivial_copy<T>::value元功能才返回true

让我们再看一些例子:

#include <iostream> 
#include <boost/type_traits/is_unsigned.hpp> 
#include <boost/type_traits/is_same.hpp> 
#include <boost/type_traits/remove_const.hpp> 

template <class T1, class T2> 
void type_traits_examples(T1& /*v1*/, T2& /*v2*/)  { 
    // Returns true if T1 is an unsigned number 
    std::cout << boost::is_unsigned<T1>::value; 

    // Returns true if T1 has exactly the same type, as T2 
    std::cout << boost::is_same<T1, T2>::value; 

    // This line removes const modifier from type of T1\. 
    // Here is what will happen with T1 type if T1 is: 
    // const int => int 
    // int => int 
    // int const volatile => int volatile 
    // const int& => const int& 
    typedef typename boost::remove_const<T1>::type t1_nonconst_t; 
}

Some compilers may compile this code even without the typename keyword, but such behavior violates the C++ standard, so it is highly recommended to write typename.

还有更多...

BOOST_STATIC_ASSSERT宏有一个更详细的变体叫做BOOST_STATIC_ASSSERT_MSG,如果断言失败,它会尽力在编译器日志(或 IDE 窗口)中输出一条错误消息。看看下面的代码:

template <class T, std::size_t BufSizeV> 
void serialize2(const T& value, boost::array<byte_t, BufSizeV>& buf) { 
    BOOST_STATIC_ASSERT_MSG(boost::has_trivial_copy<T>::value, 
        "This serialize2 function may be used only " 
        "with trivially copyable types." 
    ); 

    BOOST_STATIC_ASSERT_MSG(BufSizeV >= sizeof(value), 
        "Can not fit value to buffer. " 
        "Make the buffer bigger." 
    ); 

    std::memcpy(&buf[0], &value, sizeof(value)); 
} 

int main() { 
    // Somewhere in code: 
    boost::array<unsigned char, 1> buf; 
    serialize2(std::string("Hello word"), buf);
}

上述代码将在 C++ 11 模式下在 g++ 编译器上编译时给出以下结果:

boost/static_assert.hpp:31:45: error: static assertion failed: This serialize2 function may be used only with trivially copyable types.
 #     define BOOST_STATIC_ASSERT_MSG( ... ) static_assert(__VA_ARGS__)
 ^
Chapter04/01_static_assert/main.cpp:76:5: note: in expansion of macro ‘BOOST_STATIC_ASSERT_MSG;
 BOOST_STATIC_ASSERT_MSG(boost::has_trivial_copy<T>::value,
 ^~~~~~~~~~~~~~~~~~~~~~~

boost/static_assert.hpp:31:45: error: static assertion failed: Can not fit value to buffer. Make the buffer bigger.
 #     define BOOST_STATIC_ASSERT_MSG( ... ) static_assert(__VA_ARGS__)
 ^
Chapter04/01_static_assert/main.cpp:81:5: note: in expansion of macro ‘BOOST_STATIC_ASSERT_MSG;
 BOOST_STATIC_ASSERT_MSG(BufSizeV >= sizeof(value),
 ^~~~~~~~~~~~~~~~~~~~~~~

无论是BOOST_STATIC_ASSSERT,还是BOOST_STATIC_ASSSERT_MSG,或者任何类型特征实体都不意味着运行时损失。所有这些函数都是在编译时执行的,不会向生成的二进制文件中添加一条汇编指令。C++ 11 标准的static_assert(condition, "message")相当于 Boost 的BOOST_STATIC_ASSSERT_MSG。在编译时不需要用户提供消息就可以断言的BOOST_STATIC_ASSERT功能在 C++ 17 中以static_assert(condition)的形式提供。您不必包含头文件就可以使用内置的编译器static_assert

Boost.TypeTraits库被部分接受为 C++ 11 标准。因此,你可以在std::名字空间的<type_traits>标题中找到特征。C++ 11 <type_traits>有一些在Boost.TypeTraits中不存在的功能,但是其他一些元功能只存在于 Boost 中。名称以has_开头的元功能在标准库中被重命名为名称以is_开头的元功能。这样,has_trivial_copy就变成了is_trivially_copyable等等。

C++ 14 和 Boost 1.65 对所有有::type成员的类型特征都有快捷键。那些快捷方式允许你写remove_const_t<T1>而不是typename remove_const<T1>::type。请注意,在 Boost 1.65 的情况下,快捷键需要 C++ 11 兼容的编译器,因为它们只能使用类型别名来实现:

template <class T>
using remove_const_t = typename remove_const<T>::type;

C++ 17 为具有::value的类型特征增加了_v快捷键。从 C++ 17 开始,可以只写std::is_unsigned_v<T1>不写std::is_unsigned<T1>::value。这一招通常使用variable templates来实现:

template <class T>
inline constexpr bool is_unsigned_v = is_unsigned<T>::value;

当 Boost 和标准库中有类似的特性时,如果您正在编写一个必须在 C++ 11 之前的编译器上工作的项目,请选择 Boost 版本。否则,在极少数情况下,标准库版本可能会稍微好一些。

请参见

  • 本章的下一个食谱将会给你更多关于静态断言和类型特征如何被使用的例子和想法。
  • 阅读Boost.StaticAssert的官方文档,了解更多示例:

http://boost.org/libs/static_assert.

启用积分类型的函数模板使用

当我们有一个实现某些功能的类模板时,这是一种常见的情况:

// Generic implementation.
template <class T> 
class data_processor { 
    double process(const T& v1, const T& v2, const T& v3); 
};

现在,假设我们有该类的两个附加版本,一个用于整型,另一个用于实型:

// Integral types optimized version. 
template <class T>
class data_processor_integral {
    typedef int fast_int_t;
    double process(fast_int_t v1, fast_int_t v2, fast_int_t v3);
}; 

// SSE optimized version for float types.
template <class T>
class data_processor_sse {
    double process(double v1, double v2, double v3);
};

现在的问题是:如何让编译器自动为指定的类型选择正确的类?

准备好

这个食谱需要一些 C++ 模板的知识。

怎么做...

我们将使用Boost.CoreBoost.TypeTraits来解决问题:

  1. 让我们从包含标题开始:
#include <boost/core/enable_if.hpp>
#include <boost/type_traits/is_integral.hpp>
#include <boost/type_traits/is_float.hpp>
  1. 让我们在通用实现中添加一个带有默认值的附加模板参数:
// Generic implementation.
template <class T, class Enable = void>
class data_processor {
    // ...
};
  1. 按照以下方式修改优化版本,以便编译器现在将它们视为模板部分专门化:
// Integral types optimized version.
template <class T>
class data_processor<
    T,
    typename boost::enable_if_c<boost::is_integral<T>::value >::type
>
{
    // ...
};

// SSE optimized version for float types.
template <class T>
class data_processor<
    T,
    typename boost::enable_if_c<boost::is_float<T>::value >::type
>
{
    // ...
};
  1. 就这样!现在,编译器将自动选择正确的类:
template <class T>
double example_func(T v1, T v2, T v3) {
    data_processor<T> proc;
    return proc.process(v1, v2, v3);
}

int main () {
    // Integral types optimized version
    // will be called.
    example_func(1, 2, 3);
    short s = 0;
    example_func(s, s, s);

    // Real types version will be called.
    example_func(1.0, 2.0, 3.0);
    example_func(1.0f, 2.0f, 3.0f);

    // Generic version will be called.
    example_func("Hello", "word", "processing");
}

它是如何工作的...

boost::enable_if_c模板是一个棘手的模板。它利用了替换失败不是错误 ( SFINAE )原理,该原理在模板实例化中使用。这就是原理的工作原理;如果在函数或类模板的实例化过程中形成了无效的参数或返回类型,则实例化将从重载解析集中移除,并且不会导致编译错误。现在棘手的部分,boost::enable_if_c<true>有一个可通过::type访问的成员类型,但是boost::enable_if_c<false>没有::type。让我们回到我们的解决方案,看看 SFINAE 如何处理作为T参数传递给data_processor类的不同类型。

如果我们传递一个int作为T类型,首先编译器将尝试从步骤 3 实例化模板部分专门化,然后使用我们的非专门化泛型版本。当它试图实例化一个float版本时,boost::is_float<T>::value元功能返回falseboost::enable_if_c<false>::type元功能不能正确实例化,因为boost::enable_if_c<false>没有::type,那是 SFINAE 作用的地方。因为类模板不能被实例化,这必须被解释为不是一个错误,编译器跳过这个模板专门化。下一个部分专门化是为整型优化的专门化。boost::is_integral<T>::value元功能返回trueboost::enable_if_c<true>::type可以实例化,使得整个data_processor特殊化的实例化成为可能。编译器找到了匹配的部分专门化,因此它不需要尝试实例化非专门化的方法。

现在,让我们尝试传递一些非算术类型(例如,const char *),让我们看看编译器会做什么。首先,编译器尝试实例化模板部分专门化。带有is_float<T>::valueis_integral<T>::value的专门化未能实例化,因此编译器尝试实例化我们的泛型版本并成功。

没有boost::enable_if_c<>,对于任何类型,所有部分专门化的版本都可能同时被实例化,导致歧义和编译失败。

If you are using templates and compiler reports that cannot choose between two template classes of methods, you probably need boost::enable_if_c<>.

还有更多...

这个方法的另一个版本叫做boost::enable_if,末尾没有_c。两者的区别在于enable_if_c接受常量作为模板参数;短版本接受具有value静态成员的对象。比如boost::enable_if_c<boost::is_integral<T>::value >::type等于boost::enable_if<boost::is_integral<T> >::type

Before Boost 1.56 the boost::enable_if metafunctions were defined in the header <boost/utility/enable_if.hpp> instead of <boost/core/enable_if.hpp>.

C++ 11 在<type_traits>头中定义了std::enable_if,其行为与boost::enable_if_c完全一样。它们之间没有区别,只是 Boost 的版本也可以在非 C++ 11 编译器上工作,提供了更好的可移植性。

C++ 14 有一个快捷方式std::enable_if_t,没有typename::type必须使用:

template <class T> 
class data_processor<
    T, std::enable_if_t<boost::is_float<T>::value >
>;

所有的使能函数只在编译时执行,不会在运行时增加性能开销。然而,添加额外的模板参数可能会在typeid(your_class).name()中产生更大的类名,并在某些平台上比较两个typeid()结果时增加极其微小的性能开销。

请参见

禁用真实类型的函数模板使用

我们继续使用 Boost 元编程库。在前面的食谱中,我们看到了如何将enable_if_c用于类;现在是时候看看它在模板函数中的用法了。

想象一下,在您的项目中,您有一个可以处理所有可用类型的模板函数:

template <class T> 
T process_data(const T& v1, const T& v2, const T& v3);

那个功能存在很久了。您已经编写了大量使用它的代码。突然,你想出了一个优化版本的process_data功能,但只针对有T::operator+=(const T&)的类型:

template <class T> 
T process_data_plus_assign(const T& v1, const T& v2, const T& v3);

您有一个庞大的代码库,对于拥有正确运算符的类型,手动将process_data更改为process_data_plus_assign可能需要几个月的时间。因此,您不想更改已经编写的代码。相反,如果可能的话,您希望强制编译器自动使用优化的函数来代替默认函数。

准备好

阅读之前的食谱,了解boost::enable_if_c的作用,并了解 SFINAE 的概念。仍然需要模板的基本知识。

怎么做...

模板魔术可以使用 Boost 库来完成。让我们看看怎么做:

  1. 我们需要boost::has_plus_assign<T>元功能和<boost/enable_if.hpp>标题:
#include <boost/core/enable_if.hpp>
#include <boost/type_traits/has_plus_assign.hpp>
  1. 现在,我们使用plus assign运算符禁用类型的默认实现:
// Modified generic version of process_data
template <class T>
typename boost::disable_if_c<boost::has_plus_assign<T>::value,T>::type
    process_data(const T& v1, const T& v2, const T& v3);
  1. 使用plus assign操作符启用类型的优化版本:
// This process_data will call a process_data_plus_assign.
template <class T>
typename boost::enable_if_c<boost::has_plus_assign<T>::value, T>::type
    process_data(const T& v1, const T& v2, const T& v3)
{
    return process_data_plus_assign(v1, v2, v3);
}
  1. 现在,尽可能使用优化版本:
int main() {
    int i = 1;
    // Optimized version.
    process_data(i, i, i);

    // Default version.
    // Explicitly specifing template parameter.
    process_data<const char*>("Testing", "example", "function");
}

它是如何工作的...

如果bool_value等于true,则boost::disable_if_c<bool_value>::type元功能禁用该方法。它的工作原理和boost::enable_if_c<!bool_value>::type一样。

在替换成功的情况下,作为第二个参数传递给boost::enable_if_cboost::disable_if_c的类通过::type返回。换句话说,boost::enable_if_c<true, T>::typeT是一样的。

让我们一步一步来看process_data(i, i, i)的情况。我们传递一个int作为T类型,编译器搜索函数process_data(int, int, int)。因为没有这样的函数,下一步就是实例化process_data的模板版本。不过有两个模板process_data功能。例如,编译器从我们的第二个(优化的)版本开始实例化模板;在这种情况下,它成功地评估了typename boost::enable_if_c<boost::has_plus_assign<T>::value, T>::type表达式,并获得了T返回类型。但是,编译器并没有停止;它继续实例化尝试,并尝试实例化我们的函数的第一个版本。在替换typename boost::disable_if_c<boost::has_plus_assign<T>::value的过程中,出现故障,根据 SFINAE 规则,该故障不被视为错误。不再有模板process_data函数,所以编译器停止实例化。如您所见,如果没有enable_if_cdisable_if_c,编译器将能够实例化这两个模板,并且会有歧义。

还有更多...

enable_if_cenable_if的情况一样,禁用功能有一个disable_if版本:

// First version 
template <class T> 
typename boost::disable_if<boost::has_plus_assign<T>, T>::type 
    process_data2(const T& v1, const T& v2, const T& v3); 

// process_data_plus_assign 
template <class T> 
typename boost::enable_if<boost::has_plus_assign<T>, T>::type 
    process_data2(const T& v1, const T& v2, const T& v3);

C++ 11 既没有disable_if_c也没有disable_if,但是你可以自由使用std::enable_if<!bool_value>::type来代替。

Before Boost 1.56 the boost::disable_if metafunctions were defined in the header <boost/utility/enable_if.hpp> instead of <boost/core/enable_if.hpp>.

正如在前面的配方中提到的,所有启用和禁用功能都只在编译时执行,不会增加运行时的性能开销。

请参见

  • 从头开始阅读本章,以获得更多编译时技巧的示例。
  • 考虑阅读Boost.TypeTraits官方文档,了解更多示例和位于http://boost.org/libs/type_traits.的元功能的完整列表
  • Boost.Core库可能会给你提供更多boost::enable_if用法的例子;在http://boost.org/libs/core.阅读

从数字创建类型

我们现在已经看到了如何使用boost::enable_if_c在函数之间进行选择的例子。让我们忘记这一章的技巧,使用不同的方法。考虑以下示例,其中我们有一个处理 POD 数据类型的通用方法:

#include <boost/static_assert.hpp> 
#include <boost/type_traits/is_pod.hpp> 

// Generic implementation. 
template <class T> 
T process(const T& val) { 
    BOOST_STATIC_ASSERT((boost::is_pod<T>::value)); 
    // ... 
}

我们还针对 1、4 和 8 字节的大小优化了一些处理功能。我们如何重写process函数,使其能够将调用分派给优化的处理函数?

准备好

强烈建议至少阅读本章的第一个食谱,这样你就不会被这里发生的所有事情所迷惑。模板和元编程不会吓到你(或者只是准备看很多)。

怎么做...

我们将看到模板类型的大小如何转换成某种类型的变量,以及该变量如何用于推导正确的函数重载。

  1. 让我们定义process_impl函数的通用和优化版本:
#include <boost/mpl/int.hpp> 

namespace detail {
    // Generic implementation.
    template <class T, class Tag>
    T process_impl(const T& val, Tag /*ignore*/) {
        // ...
    }

    // 1 byte optimized implementation.
    template <class T>
    T process_impl(const T& val, boost::mpl::int_<1> /*ignore*/) {
        // ...
    }

    // 4 bytes optimized implementation.
    template <class T>
    T process_impl(const T& val, boost::mpl::int_<4> /*ignore*/) {
        // ...
    }

    // 8 bytes optimized implementation.
    template <class T>
    T process_impl(const T& val, boost::mpl::int_<8> /*ignore*/) {
        // ...
    }
} // namespace detail
  1. 现在,我们准备编写一个流程函数:
// Dispatching calls:
template <class T>
T process(const T& val) {
    BOOST_STATIC_ASSERT((boost::is_pod<T>::value));
    return detail::process_impl(val, boost::mpl::int_<sizeof(T)>());
}

它是如何工作的...

这里最有意思的是boost::mpl::int_<sizeof(T)>()sizeof(T)在编译时执行,所以它的输出可以作为模板参数。类boost::mpl::int_<>只是一个保存整型编译时值的空类。在Boost.MPL库中,这样的类被称为积分常数。它可以按照下面的代码实现:

template <int Value> 
struct int_ { 
    static const int value = Value; 
    typedef int_<Value> type; 
    typedef int value_type; 
};

我们需要这个类的一个实例,这就是为什么我们在boost::mpl::int_<sizeof(T)>()的末尾有一个圆括号。

现在,让我们仔细看看编译器将如何决定使用哪个process_impl函数。首先,编译器试图匹配具有非模板第二参数的函数。如果sizeof(T)是 4,编译器会尝试搜索带有像process_impl(T, boost::mpl::int_<4>)这样的签名的函数,并从detail命名空间中找到我们的 4 字节优化版本。如果sizeof(T)是 34,编译器找不到签名像process_impl(T, boost::mpl::int_<34>)的函数,使用模板函数process_impl(const T& val, Tag /*ignore*/)

还有更多...

Boost.MPL库有几种元编程的数据结构。在这个食谱中,我们只触及了冰山一角。您可能会发现 MPL 中的以下积分常数类很有用:

  • bool_
  • int_
  • long_
  • size_t
  • char_

所有的Boost.MPL函数(除了for_each运行时函数)都是在编译时执行的,不会增加运行时开销。 T2 库不是 C++ 的一部分。然而,C++ 重用了该库中的许多技巧。头文件type_traits中的 C++ 11 有一个std::integral_constant<type, value>类,可以用与前面例子相同的方式使用。您甚至可以使用它定义自己的类型别名:

template <int Value>
using int_ = std::integral_constant<int, Value>;

请参见

实现类型特征

我们需要实现一个类型特征,如果将std::vector类型作为模板参数传递给它,则返回true,否则返回false

准备好

需要一些Boost.TypeTrait或标准库类型特征的基本知识。

怎么做...

让我们看看如何实现类型特征:

#include <vector> 
#include <boost/type_traits/integral_constant.hpp> 

template <class T> 
struct is_stdvector: boost::false_type {}; 

template <class T, class Allocator> 
struct is_stdvector<std::vector<T, Allocator> >: boost::true_type  {};

它是如何工作的...

几乎所有的工作都是由boost::true_typeboost::false_type班完成的。boost::true_type类中有一个布尔::value静态常数,等于trueboost::false_type类中有一个布尔::value静态常数等于false。这两个班也有一些typedef s 配合好Boost.MPL库。

我们的第一个is_stdvector结构是一个通用结构,当没有找到这种结构的模板专用版本时,它将一直被使用。我们的第二个is_stdvector结构是针对std::vector类型的模板专门化(注意它是从true_type派生的)。因此,当我们将std::vector类型传递给is_stdvector结构时,编译器会选择一个模板专用版本。如果我们传递除std::vector以外的数据类型,则使用从false_type派生的通用版本。

There is no public keyword before boost::false_type and, boost::true_type in our trait, because we use struct keyword, and by default, it uses public inheritance.

还有更多...

使用 C++ 11 兼容编译器的读者可以使用在<type_traits>头中声明的true_typefalse_type类型来创建他们自己的类型特征。从 C++ 17 开始,标准库有一个bool_constant<true_or_false>类型别名,为了方便您可以使用。 像往常一样,类和函数的 Boost 版本更具可移植性,因为它们可以在 C++ 11 之前的编译器上使用。

请参见

为模板参数选择最佳运算符

想象一下,我们正在使用来自不同供应商的类,这些类实现不同数量的算术运算,并具有整数构造函数。我们确实希望创建一个函数,通过传递给它的任何一个类来递增。还有,我们希望这个功能有效!看看下面的代码:

template <class T> 
void inc(T& value) { 
    // TODO:
    // call ++ value 
    // or call value ++ 
    // or value += T(1); 
    // or value = value + T(1); 
}

准备好

需要一些 C++ 模板的基础知识,以及Boost.TypeTrait或标准库类型特征。

怎么做...

所有的选择都可以在编译时完成。这可以使用Boost.TypeTraits库来实现,如以下步骤所示:

  1. 让我们从制作正确的功能对象开始:
namespace detail {
    struct pre_inc_functor {
    template <class T>
        void operator()(T& value) const {
           ++ value;
        }
    };

    struct post_inc_functor {
    template <class T>
        void operator()(T& value) const {
            value++ ;
        }
    };

    struct plus_assignable_functor {
    template <class T>
        void operator()(T& value) const {
            value += T(1);
        }
    };

    struct plus_functor {
    template <class T>
        void operator()(T& value) const {
            value = value + T(1);
        }
    };
}
  1. 之后,我们将需要一堆类型特征:
#include <boost/type_traits/conditional.hpp>
#include <boost/type_traits/has_plus_assign.hpp>
#include <boost/type_traits/has_plus.hpp>
#include <boost/type_traits/has_post_increment.hpp>
#include <boost/type_traits/has_pre_increment.hpp>
  1. 我们准备推导出正确的函子并使用它:
template <class T>
void inc(T& value) {
    // call ++ value
    // or call value ++
    // or value += T(1);
    // or value = value + T(1);

    typedef detail::plus_functor step_0_t;

    typedef typename boost::conditional<
      boost::has_plus_assign<T>::value,
      detail::plus_assignable_functor,
      step_0_t
    >::type step_1_t; 

    typedef typename boost::conditional<
      boost::has_post_increment<T>::value,
      detail::post_inc_functor,
      step_1_t
    >::type step_2_t;

    typedef typename boost::conditional<
      boost::has_pre_increment<T>::value,
      detail::pre_inc_functor,
      step_2_t
    >::type step_3_t;

    step_3_t() // Default construction of the functor.
        (value); // Calling operator() of the functor.
}

它是如何工作的...

所有的魔法都是通过conditional<bool Condition, class T1, class T2>元功能完成的。当true作为第一个参数传入元功能时,它通过::type typedef返回T1。当false作为第一个参数传入元功能时,通过::type typedef返回T2。它的行为类似于某种编译时if语句。

所以,step0_t持有detail::plus_functor元功能,step1_t持有step0_tdetail::plus_assignable_functorstep2_t型保持step1_tdetail::post_inc_functorstep3_t型适用于step2_tdetail::pre_inc_functor。每个step*_t typedef持有的是用类型性状推导出来的。

还有更多...

这个函数有一个 C++ 11 版本,可以在std::命名空间的<type_traits>头中找到。Boost 在不同的库中有这个函数的多个版本;例如,Boost.MPL有功能boost::mpl::if_c,作用和boost::conditional一模一样。它还有一个版本boost::mpl::if_(结尾没有c),第一个模板参数叫::type;如果它是从boost::true_type派生的,它会在::type调用期间返回它的第二个参数。否则,它将返回最后一个模板参数。我们可以重写我们的inc()函数来使用Boost.MPL,如下面的代码所示:

#include <boost/mpl/if.hpp> 

template <class T> 
void inc_mpl(T& value) { 
    typedef detail::plus_functor step_0_t;

    typedef typename boost::mpl::if_<
      boost::has_plus_assign<T>,
      detail::plus_assignable_functor,
      step_0_t
    >::type step_1_t;

    typedef typename boost::mpl::if_<
      boost::has_post_increment<T>,
      detail::post_inc_functor,
      step_1_t
    >::type step_2_t;

    typedef typename boost::mpl::if_<
      boost::has_pre_increment<T>,
      detail::pre_inc_functor,
      step_2_t
    >::type step_3_t;

    step_3_t()   // Default construction of the functor.
        (value); // Calling operator() of the functor.
}

C++ 17 有一个if constexpr结构,使得前面的例子简单得多:

template <class T> 
void inc_cpp17(T& value) { 
    if constexpr (boost::has_pre_increment<T>()) {
        ++ value;
    } else if constexpr (boost::has_post_increment<T>()) {
        value++ ;
    } else if constexpr(boost::has_plus_assign<T>()) {
        value += T(1);
    } else {
        value = value + T(1);
    }
}

Integral constants in the standard library, Boost.MPL and Boost.TypeTraits have a constexpr conversion operator. For example, it means that an instance of std::true_type can be converted to true value. In the preceding example, boost::has_pre_increment<T> denotes a type, appending (), or C++ 11 curly brackets {} make an instance of that type, that is convertible to true or false values.

请参见

  • 配方启用积分类型的模板功能使用。
  • 配方禁用真实类型的模板功能使用。
  • Boost.TypeTraits文档有可用元功能的完整列表。跟随链接http://boost.org/libs/type_traits阅读。
  • 第八章元编程中的食谱会给你更多Boost.MPL库用法的例子。如果你有信心,你也可以试着在http://boost.org/libs/mpl链接阅读它的文档。

在 C++ 03 中获取表达式类型

在之前的食谱中,我们看到了一些boost::bind用法的例子。它在 C++ 11 字之前可能是一个有用的工具,但是在 C++ 03 中很难将boost::bind结果存储为变量。

#include <functional> 
#include <boost/bind.hpp> 

const ??? var = boost::bind(std::plus<int>(), _1, _1);

在 C++ 11 中,我们可以用auto关键字代替???,这样就可以了。C++ 03 有没有办法做到?

准备好

C++ 11 autodecltype关键词的知识可能会帮助你理解这个食谱。

怎么做...

我们需要一个Boost.Typeof库来获取表达式的返回类型:

#include <boost/typeof/typeof.hpp>

BOOST_AUTO(var, boost::bind(std::plus<int>(), _1, _1));

它是如何工作的...

它只是创建了一个名为var的变量,表达式的值作为第二个参数传递。var的类型是从表达类型中检测出来的。

还有更多...

一个有经验的 C++ 读者会注意到,在 C++ 11 中有更多的关键字用于检测表达式的类型。也许Boost.Typeof对他们也有宏指令。让我们看看下面的 C++ 11 代码:

typedef decltype(0.5 + 0.5f) type;

使用Boost.Typeof,前面的代码可以按照如下方式编写:

typedef BOOST_TYPEOF(0.5 + 0.5f) type;

C++ 11 版本的decltype(expr)推导并返回expr的类型。

template<class T1, class T2> 
auto add(const T1& t1, const T2& t2) ->decltype(t1 + t2) { 
    return t1 + t2; 
};

使用Boost.Typeof,前面的代码可以这样写:

// Long and portable way:
template<class T1, class T2>
struct result_of {
    typedef BOOST_TYPEOF_TPL(T1() + T2()) type;
};

template<class T1, class T2>
typename result_of<T1, T2>::type add(const T1& t1, const T2& t2) {
    return t1 + t2;
};

// ... or ...

// Shorter version that may crush some compilers.
template<class T1, class T2>
BOOST_TYPEOF_TPL(T1() + T2()) add(const T1& t1, const T2& t2) {
    return t1 + t2;
};

C++ 11 has a special syntax for specifying return type at the end of the function declaration. Unfortunately, this cannot be emulated in C++ 03, so we cannot use t1 and t2 variables in a macro.

您可以在模板和任何其他编译时表达式中自由使用BOOST_TYPEOF()函数的结果:

#include <boost/static_assert.hpp> 
#include <boost/type_traits/is_same.hpp> 
BOOST_STATIC_ASSERT((
    boost::is_same<BOOST_TYPEOF(add(1, 1)), int>::value
));

然而不幸的是,没有帮助,这种魔力并不总是有效的。例如,用户定义的类并不总是被检测到,因此以下代码在某些编译器上可能会失败:

namespace readers_project { 
    template <class T1, class T2, class T3> 
    struct readers_template_class{}; 
} 

#include <boost/tuple/tuple.hpp> 

typedef 
    readers_project::readers_template_class<int, int, float> 
readers_template_class_1; 

typedef BOOST_TYPEOF(boost::get<0>( 
    boost::make_tuple(readers_template_class_1(), 1) 
)) readers_template_class_deduced; 

BOOST_STATIC_ASSERT(( 
    boost::is_same< 
        readers_template_class_1, 
        readers_template_class_deduced 
    >::value 
));

在这种情况下,你可以向Boost.Typeof伸出援助之手,注册一个模板:

BOOST_TYPEOF_REGISTER_TEMPLATE( 
        readers_project::readers_template_class /*class name*/, 
        3 /*number of template classes*/ 
)

然而,即使没有BOOST_TYPEOF_REGISTER_TEMPLATE和没有 C++ 11,三个最流行的编译器也能正确检测出类型。

请参见

Boost.Typeof的官方文档有更多的例子。跟随链接http://boost.org/libs/typeof阅读。