std :: visit是现代C ++的所有错误(2017)

2020-12-05 22:09:44

一个求和类型,也称为区分联合,可以容纳几种类型的事物中的一种(并且只有一种),例如,考虑类似INI的配置文件中的某些设置,假设每个设置必须是一个字符串,一个整数或布尔值。如果要在C ++中推出自己的解决方案,我们可能会写类似以下内容:

结构设置{union {字符串str;整数;布尔b; };枚举类型{Str,Int,Bool};类型标签; }; //将设置映射到其名称。使用设置= unordered_map<字符串,设置> ;

在适当的时候调用所有非平凡类型的构造函数和析构函数。(字符串是这里唯一的类型,但是您可以想象与其他类型相似的场景。)

如果忘记了某个步骤,则该对象将进入不一致状态,并且会出现哭声和咬牙切齿。您可以封装所有这些技巧,并通过一系列方法与该类型进行交互,例如getType(),asBool(),asString (),等等。但是这很冗长。它也只是将问题转移给实现这些方法的人。他们仍然需要在没有语言帮助的情况下仔细维护不变式。

如果标准库提供了通用的求和类型,那就更好了。在C ++ 17中,我们终于得到了一个!它叫做std :: variant。让我们来看一下。

Variant是一个类模板,使用它可以容纳的类型作为模板参数。对于上面的示例,我们可以将设置定义为Variant< string,int,bool>将值赋值给变量可以像您期望的那样工作:

一旦将值放入变体中,我们最终将要查看该值是什么,同样重要的是,该值的类型是什么。这就是乐趣所在。某些语言提供了专用的模式匹配语法任务,例如:

match(theSetting){Setting :: Str(s)=> println! ("字符串:{}",s),设置:: Int(n)=> println! ("整数:{}",n),设置:: Bool(b)=> println! (" A boolean:{}",b),};

但这并没有降低C ++ 17的工作量。 2相反,我们提供了一个名为std :: visit的伴随函数。它带有您要检查的变体,以及可为该变体中的每种类型调用的某些访问者。

我们如何定义这样的访问者?一种方法是创建一个使相关类型的调用运算符超载的对象:

struct SettingVisitor {void operator()(const string& s)const {printf(" A string:%s \ n",s。c_str()); } void运算符()(const int n)const {printf("整数:%d \ n",n); } void运算符()(const bool b)const {printf(" A布尔值:%d \ n",b); };

这似乎太冗长了,如果我们希望访问者捕获或修改其他状态,情况就更糟了。嗯,lambda非常适合捕获状态。如果我们可以从那些访问者中建立访问者呢?

make_visitor([&](const string& s){printf(" string:%s \ n",s。c_str()); // ...},[&]( const int d){printf(" integer:%d \ n",d); // ...},[&](const bool b){printf(" bool: %d \ n",b); // ...})

这样做会更好一些,但是标准库没有提供任何make_visitor来将lambda组合成可调用对象,我们需要自己定义。

模板<类... FS结构重载;模板< F0级,... Frest级结构重载< F0,Frest ...> :F0,过载<弗雷斯特... {过载(F0 f0,Frest ... rest):F0(f0),过载<弗雷斯特... (rest ...){}使用F0 ::运算符();使用过载< Frest ...> ::运算符(); };模板< F0级结构重载< F0 :F0 {重载(F0 f0):F0(f0){}使用F0 ::运算符(); };模板<类... FS自动make_visitor(Fs ... fs){返回过载< Fs ...> (fs ...); }

这里我们使用C ++ 11的可变参数模板,必须递归定义它们,因此我们创建了一些基本情况F0,然后使用它来定义用于重载的级联构造函数集,每个构造函数都剥离一个lambda参数并将其添加到输入呼叫操作员。

如果这看起来很麻烦,请不要担心! C ++ 17将提供新的语法,将上述所有内容简化为:

容易吧?但是,如果不喜欢这些选项,则可以使用C ++ 17的编译时条件:

[](auto& arg){使用T = std ::衰落_t< decltype(arg)> ;如果constexpr(std :: is_same_v< T,字符串>){printf(" string:%s \ n",arg。c_str()); // ...}否则,如果constexpr(std :: is_same_v< T,int>){printf(" integer:%d \ n",arg); // ...}如果constexpr(std :: is_same_v< T,bool>){printf(" bool:%d \ n",arg); // ...}}

std :: visit所需的严格知识简直太疯狂了,我们从一个简单的目标开始:查看sum类型的内容,要完成这个微薄的任务,我们必须:

使用编译时条件,这需要您了解和了解新的constexpr if语法,以及type_traits的乐趣,如std :: decay。

如果您是经验丰富的C ++开发人员,这些概念都不是太神秘,但是肯定有一些是该语言的“高级”功能。如果我们需要知道太多的知识来做这么简单的事情,那么事情真的就已经横竖了。

我的目标不是要贬低采用这种方法的ISO C ++委员会的成员。我与其中一些人在一起喝啤酒,他们是聪明,善良,勤奋的人。我敢肯定,我缺少重要的背景信息因为我从未参加过标准会议或从未阅读过所有相关的委员会文件。但是从局外人的角度来看,要解决的问题(“这里有什么?”)与解决方案之间的复杂性差距仅仅是个错误。您可以在不让初学者和其他所有东西令人不知所措的情况下进行教学吗?这对您的日常程序员来说应该是常识吗?(并且如果向标准库中添加变体的目的不是使其成为大众的工具, (不是吗?)如果委员会没有足够的时间或资源来使模式与语言匹配,C ++ 17至少可以做的是提供类似于make_visitor的功能。用户。

如果我不得不猜测我们的结局如何,我认为这可能归因于确认偏见。也许当一群真正精明的人知道SFINAE的工作方式,而当看到诸如此类的东西时却不退缩

聚在一起,结果就像是std :: visit。没有人宣称皇帝没有衣服,或者完全期望普通用户使用递归模板来构建一个超载的可调用对象,只是为了看看他们正在寻找的东西一个int或一个字符串。

我并不是在这里并不是说C ++本身就太复杂了,但是肯定比它要复杂得多.Scott Meyers,写了EffectiveC ++和Effective Modern C ++的人在最近的演讲中也发出了类似的声音。用梅耶斯的话来说,我确信委员会的每个成员都非常关心避免不必要的复杂性并使语言更易于使用,但是如果您查看他们的工作成果,就很难说了。偶然的复杂性只会不断堆积向上。

C ++如此广泛地使用是有原因的,尤其是在系统编程中。 3它的表现力令人难以置信,但可以让您几乎完全控制硬件。围绕它的工具是目前所有编程语言中最成熟的一些,C栏除外。它支持数量众多的平台。

但是即使您放弃了所有的历史包bag,它也有一些严重的缺点。花任何时间与D纠缠不清,您会很快意识到元编程不需要自我鞭and和疯狂的语法。感觉就像unique_ptrand shared_ptr(它们本身一直是新鲜空气)是一个不好玩的笑话。事实上,我们仍然在2017年通过使用#includemacros相互复制粘贴文件来处理依赖项,这是令人讨厌的。

根据ISO标准的最终结果以及在会议上的讲话,您会得到一种印象,即使用C ++的驱动程序正在试图通过将其他语言的精妙之处消除一些缺陷,这是一个很好的主意,但是这些功能似乎似乎有些迟钝。尽管C ++不会很快消失,但感觉像是该语言在不断地扮演着笨拙的追赶游戏的角色。

尽管如此,我还是会一直鼓励我的同事在有人需要的时候使用变异。求和类型是一个有用的概念,值得付出痛苦,并引用乔恩·卡尔布的话:“如果你做不到用丑陋的语言编写程序,也许C ++不是您应该使用的语言。”

术语“求和类型”来自基础类型理论-如果一个类型可以容纳类型A或类型B,则其可能状态的集合是A的所有可能状态与B的所有可能状态的总和,即A +B。您已经熟悉了求和类型的对偶:乘积类型,AKA结构,元组等(考虑包含A和B类型的结构的可能状态的集合是其可能状态A×B的乘积)。 ↩