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<T,
           std::void_t<decltype( ++std::declval<T&>() )>
       > : std::true_type { };

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

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

基础知识

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

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

is_incrementable<T>::value

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

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

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

template<typename T>
struct is_incrementable : std::false_type{};

template<typename T>
struct is_incrementable<something that says that T is incrementable> : std::true_type{};

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

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

选一个好名字

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

template<typename...>
using void_t = void;

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

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

template<typename T>
struct is_incrementable<T, void_t<decltype(++std::declval<T&>())>> : std::true_type{};

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

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

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

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

template<typename...>
using try_to_instantiate = void;

template<typename T>
struct is_incrementable<T, try_to_instantiate<decltype(++std::declval<T&>())>> : std::true_type{};

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

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

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

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

template<typename...>
using try_to_instantiate = void;

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

template<typename T>
struct is_incrementable<T, try_to_instantiate<decltype(++std::declval<T&>())>> : std::true_type{};

分离抽象层次

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

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

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

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

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

template<typename T, template<typename> class Expression>
struct is_detected<T, Expression, try_to_instantiate<Expression<T>>> : std::true_type{};

然后 is_incrementable 就变成了

template<typename T>
using increment_expression = decltype(++std::declval<T&>());

template<typename T>
using is_incrementable = is_detected<T, increment_expression>;

在表达式中允许几种类型

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

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

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

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

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

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

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

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

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

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

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

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

using disregard_this = void;

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

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

template<typename T, typename U>
using assign_expression = decltype(std::declval<T&>() = std::declval<U&>());

template<typename T, typename U>
using are_assignable = is_detected<assign_expression, disregard_this, T, U>

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

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

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

最终的代码如下:

template<typename...>
using try_to_instantiate = void;

using disregard_this = void;

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

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

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

这是如何使用它:

template<typename T, typename U>
using assign_expression = decltype(std::declval<T&>() = std::declval<U&>());

template<typename T, typename U>
constexpr bool is_assignable = is_detected<assign_expression, T, U>;

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

// 编译时使用
static_assert(is_assignable<int, double>, "");
static_assert(!is_assignable<int, std::string>, "");

// 运行时使用
std::cout << std::boolalpha;
std::cout << is_assignable<int, double> << '\n';
std::cout << is_assignable<int, std::string> << '\n';

编译成功,而且输出:

true
false

TMP不必那么复杂

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

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,482评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,377评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,762评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,273评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,289评论 5 373
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,046评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,351评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,988评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,476评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,948评论 2 324
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,064评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,712评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,261评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,264评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,486评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,511评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,802评论 2 345

推荐阅读更多精彩内容