Fluent C++:富有表现力的C ++模板元编程

原文

C ++开发人员中有一部分人喜欢模板元编程(TMP)。

还有其他所有C ++开发人员。

虽然我认为自己倾向于狂热者阵营。但是我遇到过的人,相比于爱好者来说,更多的人对它没有什么兴趣甚至感到厌恶。你是哪个阵营的?

在我看来,TMP之所以无法为许多人接受的原因之一是它通常很晦涩。 有时它看起来像是黑魔法,只保留给可以理解其方言的开发人员的一个非常特殊的亚种。 当然,有时我们会遇到偶尔可以理解的TMP,但是平均而言,我发现它比常规代码更难理解。

我想指出的是,TMP不必一定是这种方式。

我将向你展示如何使TMP代码更具表现力。 它并没有那么难。

TMP通常被描述为C ++语言中的一种语言。 因此,为了使TMP更具表现力,我们只需要应用与常规代码相同的规则即可。 为了说明这一点,我们将采用一段只有我们最勇敢的人才能理解的代码,并在其上应用以下两个表达性准则:

  • 选择好名字,
  • 并分离出抽象层次。

我给你说过了,它并没有那么难。

示例代码的目的

我们将编写一个API,以检查表达式对于给定类型是否有效。

例如,给定类型T,我们想知道T是否可递增,也就是说,对于类型T的对象t,如下表达式是否合法:

++t

如果T为int,则表达式有效;如果T为std :: string,则表达式无效。

这是实现它的TMP的典型部分:

template< typename, typename = void >
struct is_incrementable : std::false_type { };

template< typename T >
struct is_incrementable() )>
       > : std::true_type { };

我不知道你需要多少时间来解析此代码,但是花了我大量的时间才能全部解决。 让我们看看如何重新编写此代码,以使其更易于理解。

公平地说,我必须说,要了解TMP,你需要了解一些结构。 有点像需要了解“ if”,“ for”和函数重载以了解C ++的知识,TMP具有一些先决条件,例如“ std :: true_type”和SFINAE。 但是,如果你不认识它们,请不要担心,我将一路向你解释。

基础知识

如果您已经熟悉TMP,则可以跳到下一部分。

我们的目标是能够以这种方式查询类型:

is_incrementable::value

is_incrementable 是一种类型,具有一个公共布尔成员value,如果T是可递增的(例如T为int),则为true;否则,则为false(例如T为std :: string)。

我们将使用std :: true_type。 它是仅具有等于true的公共布尔成员值的类型。 在T可以递增的情况下,我们将从它继承 is_incrementable 。 而且,你已经猜到了,如果T不能递增,则从std :: false_type继承。

为了允许有两个可能的定义,我们使用模板特化。 一种专门继承自std :: true_type,另一种专门继承自std :: false_type。 因此,我们的解决方案将大致如下所示:

template
struct is_incrementable : std::false_type{};

template
struct is_incrementable : std::true_type{};

特化基于SFINAE。 简而言之,我们将编写一些代码,尝试在特化中自增T。 如果T确实是可递增的,则此代码将有效,特化就会实例化(因为它始终优先于主模板)。它会继承std :: true_type。

另一方面,如果T不可递增,则特化将无效。 在这种情况下,SFINAE表示无效的实例化不会停止编译。 它只是被完全丢弃,剩下的唯一模板是主模板,即从std :: false_type继承。

选一个好名字

文章顶部的代码使用了std :: void_t。 此结构出现在C ++ 17的标准中,但可以立即在C ++ 11中复制:

template
using void_t = void;

void_t只是实例化它传递的模板类型,并且从不使用它们。 如果可以的话,它就像模板的代孕母亲。

为了使代码正常工作,我们以这种方式编写特化代码:

template
struct is_incrementable())>> : std::true_type{};

好吧,要了解TMP,还需要了解decltype和declval:decltype返回其参数的类型,而declval ()的作用就像在decltype表达式中实例化了T类型的对象一样(这很有用,因为我们不需要一定知道T的构造函数是什么样的)。所以decltype(++ std :: declval ())是在T上调用的operator ++的返回类型。

如上所述,void_t只是实例化此返回类型的助手。它不携带任何数据或行为,只是一种启动板,用于实例化由decltype返回的类型。

如果增量表达式无效,则由void_t进行的实例化将失败,SFINAE启动,is_incrementable 解析为继承自std :: false_type的主模板。

这是一个很棒的机制,但我对这个名字有 异议。在我看来,这绝对是错误的抽象级别:将其实现为void,但是要做的是尝试实例化一个类型。通过将这些信息处理为代码,TMP表达式立即清晰起来:

template
using try_to_instantiate = void;

template
struct is_incrementable())>> : std::true_type{};

考虑到使用两个模板参数的特化,主模板也必须具有两个参数。 为了避免用户传值,我们提供了一个默认类型,即void。 现在的问题是如何命名该技术参数?

解决此问题的一种方法是完全不命名(顶部的代码使用了此选项):

template
struct is_incrementable : std::false_type{};

我认为这是一种说“别看这个,不相关,而且只出于技术原因”的一种方式。 另一种选择是给它起一个名字,说明它的意思。 第二个参数是尝试实例化特化形式中的表达式,因此我们可以将此信息写到名称中,从而提供到目前为止的完整解决方案:

template
using try_to_instantiate = void;

template
struct is_incrementable : std::false_type{};

template
struct is_incrementable())>> : std::true_type{};

分离抽象层次

我们可以在这里就完成了。 但是可以说 is_incrementable 中的代码仍然过于技术化,可能会被推到较低的抽象层。 此外,可以想象,在某个时候我们将需要使用相同的技术来检查其他表达式,并且最好将检查机制排除在外,以避免代码重复。

我们最终将得到类似于is_detected功能的内容。

上面代码中变化最大的部分显然是decltype表达式。 因此,让我们将其作为模板参数放到输入中。 但是,再次让我们仔细选择名称:此参数表示一个表达式类型。

此表达式本身取决于模板参数。 因此,我们不只是使用类型名作为参数,而是使用模板(因此使用template 类):

template class Expression, typename Attempt = void>
struct is_detected : std::false_type{};

template class Expression>
struct is_detected>> : std::true_type{};

然后 is_incrementable 就变成了

template
using increment_expression = decltype(++std::declval());

template
using is_incrementable = is_detected;

在表达式中允许几种类型

到目前为止,我们已经使用了仅涉及一种类型的表达式,但是能够将多种类型传递给表达式将是很好的选择。 例如,用于测试两种类型是否可相互赋值。

为此,我们需要使用可变参数模板来表示表达式中的类型。 我们想像下面的代码一样添加一些点,但是它不起作用:

template class Expression, typename Attempt = void>
struct is_detected : std::false_type{};

template class Expression>
struct is_detected>> : std::true_type{};

这是行不通的,因为可变参数包的类型名称... Ts将占用所有模板参数,因此需要将其放在最后。 但是默认模板参数Attempt也需要放在最后。 所以我们遇到一个问题。

首先,将包移至模板参数列表的末尾,然后删除“Attempt”的默认类型:

template class Expression, typename Attempt, typename... Ts>
struct is_detected : std::false_type{};

template class Expression, typename... Ts>
struct is_detected>, Ts...> : std::true_type{};

但是传递给Attempt什么类型呢?

第一反应可能是传void,因为try_to_instantiate的成功分支处理了void,因此我们需要传递它以实例化特化模板。

但是我认为这样做会使调用者挠头:传void意味着什么? 与函数的返回类型相反,void在TMP中并不表示“无”,因为void是一种类型。

因此,给它起一个更好地表达我们意图的名称。 有人称这种事情为“dummy”,但我喜欢给个更准确的名称:

using disregard_this = void;

但是我猜这个名称因人而异。

然后可以通过以下方式编写赋值检查:

template
using assign_expression = decltype(std::declval() = std::declval());

template
using are_assignable = is_detected

当然,即使disregard_this通过说我们不需要担心来让读者放心,但它仍然会放在那碍事。

一种解决方案是将其隐藏在间接级别之后:is_detected_impl。 “ impl_”通常在TMP中(以及在其他地方)也意味着“间接级别”。 虽然我觉得这个词不自然,但我想不出一个更好的名字,而且由于很多TMP代码都使用它,所以这个名字也约定俗成了。

我们还将利用这种间接级别来获取:: value属性,从而避免所有元素在每次使用它时都要调用一次。

最终的代码如下:

template
using try_to_instantiate = void;

using disregard_this = void;

template class Expression, typename Attempt, typename... Ts>
struct is_detected_impl : std::false_type{};

template class Expression, typename... Ts>
struct is_detected_impl>, Ts...> : std::true_type{};

template class Expression, typename... Ts>
constexpr bool is_detected = is_detected_impl::value;

这是如何使用它:

template
using assign_expression = decltype(std::declval() = std::declval());

template
constexpr bool is_assignable = is_detected;

生成的值可以在编译时或运行时使用。 以下程序:

// 编译时使用
static_assert(is_assignable, "");
static_assert(!is_assignable, "");

// 运行时使用
std::cout << std::boolalpha;
std::cout << is_assignable << '\n';
std::cout << is_assignable << '\n';

编译成功,而且输出:

true
false

TMP不必那么复杂

诚然,要了解TMP,需要满足一些先决条件,例如SFINAE等。 但是除此之外,没有必要把使用TMP的代码搞的比实际需要的复杂。

考虑一下现在进行单元测试的好习惯:不能因为不是生产代码,所以我们就降低质量标准。 嗯,对于TMP来说更是如此:这是生产代码。 因此,让我们将其与代码的其余部分一样对待,并尽最大努力使其表现力更好。 很有可能会吸引更多的人。 社区越丰富,好主意就越多。

你可能感兴趣的:(Fluent C++:富有表现力的C ++模板元编程)