Skip to content

Latest commit

 

History

History
225 lines (199 loc) · 7.01 KB

ch33.md

File metadata and controls

225 lines (199 loc) · 7.01 KB

Chapter33 编写泛型代码的改进

C++17引入了很多辅助工具来帮助实现泛型代码和库。

注意我们已经在类型特征扩展一章中介绍了一些新的类型特征。

33.1 std::invoke<>()

新的工具std::invoke<>()是一个新的辅助函数, 它被用于编写调用一个可调用对象的代码,可调用对象包括函数、lambda、 有operator()的函数对象、成员函数。

这里有一个辅助函数的例子演示怎么使用它:

#include <utility>      // for std::invoke()
#include <functional>   // for std::forward()

template<typename Callable, typename... Args>
void call(Callable&& op, Args&&... args)
{
    ...
    std::invoke(std::forward<Callable>(op),     // 调用传入的可调用对象
                std::forward<Args>(args)...);   // 以传入的其他参数为参数
}

你传递给call()的第一个参数,将会按照如下方式使用剩余的参数进行调用:

  • 如果可调用对象是一个成员函数的指针,将使用剩余参数中的第一个参数作为调用成员函数的对象, 所有其他参数被用作调用的参数。
  • 否则,可调用对象会把剩余参数用作自己的参数进行调用。

例如:

#include "invoke.hpp"
#include <iostream>
#include <vector>

void print(const std::vector<int>& coll)
{
    std::cout << "elems: ";
    for (const auto& elem : coll) {
        std::cout << elem << ' ';
    }
    std::cout << '\n';
}

int main()
{
    std::vector<int> vals{0, 8, 15, 42, 13, -1, 0};

    call([&vals] {
             std::cout << "size: " << vals.size() << '\n';
         });
    call(print, vals);

    call(&decltype(vals)::pop_back, vals);
    call(print, vals);

    call(&decltype(vals)::clear, vals);
    call(print, vals);
}

注意在不指明要调用哪个版本的情况下调用重载函数将导致错误:

call(&decltype(vals)::resize, vals, 5);     // ERROR:resize()被重载了

call<void(decltype(vals)::*)(std::size_t)>(&decltype(vals)::resize, vals, 5);   // OK

还要注意调用函数模板需要显式实例化。如果print()是一个模板:

template<typename T>
void print(const T& coll)
{
    std::cout << "elems: ";
    for (const auto& elem : coll) {
        std::cout << elem << ' ';
    }
    std::cout << '\n';
}

那么当你将它传给call时必须显式指明模板参数:

call(print, vals);                      // ERROR:不能推导出模板参数T

call(print<std::vector<int>>, vals);    // OK

最后,注意根据移动语义的规则,转发一个调用的结果需要使用decltype(auto)完美返回 返回值到调用者:

template<typename Callable, typename... Args>
decltype(auto) call(Callable&& op, Args&&.. args)
{
    return std::invoke(std::forward<Callable>(op),      // 调用传入的可调用对象
                       std::forward<Args>(args)...);    // 以传入的其他参数为参数
}

33.2 std::bool_constant<>

如果一个特征返回bool值,那么它们现在使用了新的模板别名bool_constant<>

namespace std {
    template<bool B>
    using bool_constant = integral_constant<bool, B>;   // 自从C++17起
    using true_type = bool_constant<true>;
    using false_type = bool_constant<false>;
}

在C++17之前,你必须直接使用integral_constant<>,这意味着true_typefalse_type按照如下方式定义:

namespace std {
    using true_type = integral_constant<bool, true>;
    using false_type = integral_constant<bool, false>;
}

bool特征仍然是在满足特定属性时继承std::true_type, 在不满足时继承std::false_type。例如:

// 主模板:T不是void类型时
template<typename T>
struct IsVoid : std::false_type {
};

// 为类型void的特化
template<>
struct IsVoid<void> : std::true_type {
};

然而,你现在可以通过派生自bool_constant<>来定义自己的类型特征, 只需要制定相应的编译期表达式作为一个bool条件。例如:

template<typename T>
struct IsLargerThanInt : std::bool_constant<(sizeof(T) > sizeof(int))> {
}

之后你可以使用这样一个特征来在编译期判断一个类型是否大于int

template<typename T>
void foo(T x)
{
    if constexpr(IsLargerThanInt<T>::value) {
        ...
    }
}

通过添加相应的内联变量:

template<typename T>
inline static constexpr auto IsLargerThanInt_v = IsLargerThanInt<T>::value;

你可以把这个特征的使用缩短为如下形式:

template<typename T>
void foo(T x)
{
    if constexpr(IsLargerThanInt_v<T>) {
        ...
    }
}

作为另一个例子,我们可以定义一个如下的特征来粗略的检查 一个类型T的移动构造函数是否保证不抛出异常:

template<typename T>
struct IsNothrowMoveConsructibleT : std::bool_constant<noexcept(T(std::declval<T>()))> {
};

33.3 std::void_t<>

还有一个很小但很有用的辅助定义类型特征的工具在C++17中被标准化了:std::void_t<>。 它简单的按照如下形式定义:

namespace std {
    template<typename...> using void_t = void;
}

也就是说,对于任何可变模板参数列表它都会返回void。 如果我们只想在参数列表中处理类型时这会很有用。

它的主要应用就是当定义新的类型特征时检查条件。 下面的例子演示了它的应用:

#include <utility>      // for declval<>
#include <type_traits>  // for true_type, false_type, void_t

// 主模板:
template<typename, typename = std::void_t<>>
struct HasVarious : std::false_type {
};

// 部分特化(may be SFINAE'd away):
template<typename T>
struct HasVarious<T, std::void_t<decltype(std::declval<T>().begin()),
                                 typename T::difference_type,
                                 typename T::iterator>>
    : std::true_type {
};

这里,我们定义了一个新的类型特征HasVariousT<>,它会检查如下三个条件:

  • 该类型有成员函数begin()吗?
  • 该类型有类型成员difference_type吗?
  • 该类型有类型成员iterator吗?

只有当对于类型T所有相应的条件都有效时才会使用部分特化版本。 在这种情况下,它的特化程度比主模板更高所以会使用它, 并且因为我们从std::true_type继承, 所以该特征的值将是true

if constexpr (HasVarious<T>::value) {
    ...
}

如果任何表达式导致无效代码(即T没有begin()、或者 没有类型成员difference_type、或者没有类型成员iterator), 部分特化版会 SFINAE'd away , 这意味着根据 代换失败不是错误(substitution failure is not an error) 规则它会被忽略。 之后,只有主模板可以使用,它派生自std::false_type,如果检查它的值 会返回false

使用这种方式,你可以使用std::void_t来轻易的定义其他检查一个或多个条件的特征, 这些条件包括是否存在某个成员或操作或者某个成员或操作的能力。 参见HasDelete<>获取另一个例子。