用户定义的文字为C ++添加了哪些新功能?


139

C ++ 11台引入了用户定义的文字,这将允许基于现有字面引入新文本语法的(inthexstringfloat),使得任何类型的将能够具有字介绍。

例子:

// imaginary numbers
std::complex<long double> operator "" _i(long double d) // cooked form
{ 
    return std::complex<long double>(0, d); 
}
auto val = 3.14_i; // val = complex<long double>(0, 3.14)

// binary values
int operator "" _B(const char*); // raw form
int answer = 101010_B; // answer = 42

// std::string
std::string operator "" _s(const char* str, size_t /*length*/) 
{ 
    return std::string(str); 
}

auto hi = "hello"_s + " world"; // + works, "hello"_s is a string not a pointer

// units
assert(1_kg == 2.2_lb); // give or take 0.00462262 pounds

乍一看,这看起来很酷,但我想知道它的实际用途是什么,当我尝试考虑使用后缀_AD_BC创建日期时,我发现由于操作员的命令这是有问题的。1974/01/06_AD会先评估1974/01(以ints开头),然后再评估(06_AD不用说八月和九月,0因为八进制原因而不必编写)。可以通过使语法为be来解决1974-1/6_AD此问题,以使运算符评估顺序有效,但是比较笨拙。

因此,我的问题归结为这是什么,您是否认为此功能可以证明其合理性?您还想定义哪些其他文字来使您的C ++代码更具可读性?


更新了语法以适合2011年6月的最终草案


8
我要投票关闭这个。标题很明显是发炎的。
小狗

76
@DeadMG,如果标题有问题,则可以对其进行编辑。尝试结束一个3年历史的问题,这个问题有11个投票和8个收藏夹,这有点有趣。(没有提到该网站的礼节在过去三年中发生了变化)。
Motti

5
我认为您的示例中有一个错误:string operator "" _s(const char*s);"不能用于解析"hello"_s"。这是一个字符串文字,将使用附加size_t参数查找运算符。我对吗?
2011年

1
我想知道的一件事是,用C ++编写“便携式C”是否有意义uint16_t,用类似的类型替换其行为是依赖于实现的行为,uwrap16unum16其行为将是与实现无关的类型,从而给定uwrap16 w=1; unum16 n=1;表达式w-2n-2将分别产生(uwrap16)65535(int)-1,[ uint16_t将在int16位的系统上产生第一个结果,而在int较大的系统上产生第二个结果]。我看到的最大问题是处理数字文字。
2015年

1
能够使数字文字与其他已定义行为的数字类型平滑地互操作,似乎应该允许这种类型用于创建一种语言,在这种语言中,想要执行依赖于实现的动作的代码可以这样做,而不必依赖于实现-定义的行为。在某些地方,由于指针差异和sizeof返回依赖于实现的整数类型之类的事情,IDB仍然是不可避免的,但是这种情况仍然可以比现在好很多。您如何看待这个概念?
2015年

Answers:


71

在这种情况下,使用用户定义的文字而不是构造函数调用会有好处:

#include <bitset>
#include <iostream>

template<char... Bits>
  struct checkbits
  {
    static const bool valid = false;
  };

template<char High, char... Bits>
  struct checkbits<High, Bits...>
  {
    static const bool valid = (High == '0' || High == '1')
                   && checkbits<Bits...>::valid;
  };

template<char High>
  struct checkbits<High>
  {
    static const bool valid = (High == '0' || High == '1');
  };

template<char... Bits>
  inline constexpr std::bitset<sizeof...(Bits)>
  operator"" _bits() noexcept
  {
    static_assert(checkbits<Bits...>::valid, "invalid digit in binary string");
    return std::bitset<sizeof...(Bits)>((char []){Bits..., '\0'});
  }

int
main()
{
  auto bits = 0101010101010101010101010101010101010101010101010101010101010101_bits;
  std::cout << bits << std::endl;
  std::cout << "size = " << bits.size() << std::endl;
  std::cout << "count = " << bits.count() << std::endl;
  std::cout << "value = " << bits.to_ullong() << std::endl;

  //  This triggers the static_assert at compile time.
  auto badbits = 2101010101010101010101010101010101010101010101010101010101010101_bits;

  //  This throws at run time.
  std::bitset<64> badbits2("2101010101010101010101010101010101010101010101010101010101010101_bits");
}

优点是将运行时异常转换为编译时错误。您不能将静态断言添加到使用字符串的位集ctor中(至少不是没有字符串模板参数时)。


7
通过为std :: bitset提供适当的constexpr构造函数,您可以执行相同的操作。
Nicol Bolas

1
@NicolBolas你是对的。我真的很惊讶一个人不在里面。如果还不算太晚,也许我们应该为2014年提议一两个。
emsr

192

乍一看,它似乎是简单的语法糖。

但是,当深入研究时,我们发现它不仅是语法糖,还扩展了C ++用户的选择范围,以创建行为类似于不同内置类型的用户定义类型。在这种情况下,这个小“红利”是C ++对C ++ 11的非常有趣的补充。

我们真的需要C ++吗?

我在过去几年编写的代码中没有看到什么用,但是仅仅因为我没有在C ++中使用它,并不意味着它对于另一位C ++开发人员来说就不那么有趣了。

我们曾在C ++(我想在C中),编译器定义的文字中使用过,将整数键入为短整数或长整数,将实数键入为float或double(甚至是long double),并将字符串作为普通或宽字符。

在C ++中,我们有可能创建自己的类型(即类),而可能没有开销(内联等)。我们可以将运算符添加到它们的类型中,使它们的行为类似于类似的内置类型,这使C ++开发人员可以像自然地将其添加到语言本身一样自然地使用矩阵和复数。我们甚至可以添加强制转换运算符(这通常不是一个好主意,但有时,这只是正确的解决方案)。

我们仍然错过了让用户类型表现为内置类型的一件事:用户定义的文字。

因此,我想这是语言的自然发展,但要尽可能完整:“ 如果您想创建一个类型,并且希望它的行为与内置类型一样多,请使用以下工具。 ..

我想这与.NET将每个基本元素都构造为结构(包括布尔值,整数等)并使所有结构派生自Object的决定非常相似。仅此一项决定就使.NET在使用基元时远远超出了Java的能力范围,无论Java将为其规范增加多少装箱/拆箱黑客。

您真的在C ++中需要它吗?

这个问题供回答。不是Bjarne Stroustrup。不是草药萨特。没有C ++标准委员会的任何成员。这就是为什么您可以在C ++中进行选择,并且它们不会将有用的表示法仅限于内置类型的原因。

如果需要它,那么它是受欢迎的补充。如果不这样做,那么...不要使用它。它不会花费您任何费用。

欢迎使用C ++,该语言的功能是可选的。

lo肿???给我看看你的情结!

肿和复杂(双关语)之间有区别。

就像Niels所展示的那样,用户定义的文字为C ++添加了哪些新功能?,能够写一个复数是“最近”添加到C和C ++的两个功能之一:

// C89:
MyComplex z1 = { 1, 2 } ;

// C99: You'll note I is a macro, which can lead
// to very interesting situations...
double complex z1 = 1 + 2*I;

// C++:
std::complex<double> z1(1, 2) ;

// C++11: You'll note that "i" won't ever bother
// you elsewhere
std::complex<double> z1 = 1 + 2_i ;

现在,可以使用运算符重载来对C99“双复数”类型和C ++“ std :: complex”类型进行乘,加,减等操作。

但是在C99中,他们只是添加了另一种类型作为内置类型,并内置了运算符重载支持。并且他们添加了另一个内置文字功能。

在C ++中,他们只是使用该语言的现有功能,看到文字功能是该语言的自然演变,因此对其进行了添加。

在C语言中,如果您需要为另一种类型使用相同的符号增强功能,那么您将很不走运,直到您进行游说以将量子波函数(或3D点,或您在工作领域中使用的任何基本类型)添加到C标准作为内置类型成功。

在C ++ 11中,您可以自己做:

Point p = 25_x + 13_y + 3_z ; // 3D point

它肿了吗?不,就在那里,正如C和C ++复杂体如何需要一种表示其文字复杂值的方式所表明的那样。

设计错误吗?不,它被设计为与其他所有C ++功能一样,并具有可扩展性。

它仅用于表示目的吗?不,因为它甚至可以为您的代码增加类型安全性。

例如,让我们想象一下一个面向CSS的代码:

css::Font::Size p0 = 12_pt ;       // Ok
css::Font::Size p1 = 50_percent ;  // Ok
css::Font::Size p2 = 15_px ;       // Ok
css::Font::Size p3 = 10_em ;       // Ok
css::Font::Size p4 = 15 ;         // ERROR : Won't compile !

这样就很容易对值的分配强制使用强类型。

是危险的吗?

好问题。这些函数可以命名空间吗?如果是的话,那么大奖!

无论如何,如果使用不当,您可能会像一切一样杀死自己。C是强大的,如果您滥用C枪,可以开枪。C ++有C枪,还有手术刀,泰瑟枪,以及您在工具包中可以找到的任何其他工具。您可以滥用手术刀并流血致死。或者,您可以构建非常优雅且健壮的代码。

那么,像每个C ++功能一样,您真的需要吗?这是在C ++中使用它之前必须回答的问题。如果您不这样做,它将不花任何费用。但是,如果您确实需要它,至少该语言不会让您失望。

日期示例?

在我看来,您的错误是您正在混合运算符:

1974/01/06AD
    ^  ^  ^

这是不可避免的,因为/是运算符,编译器必须对其进行解释。而且,AFAIK,这是一件好事。

为了找到您的问题的解决方案,我将以其他方式编写文字。例如:

"1974-01-06"_AD ;   // ISO-like notation
"06/01/1974"_AD ;   // french-date-like notation
"jan 06 1974"_AD ;  // US-date-like notation
19740106_AD ;       // integer-date-like notation

就个人而言,我会选择整数和ISO日期,但这取决于您的需求。这就是让用户定义自己的文字名称的全部目的。


1
谢谢,我和我的其他候补人物都被发现了。更严重的是,我确实独自写过这篇文章,但是也许我使用的是我的母语的一些表达方式,但它们不能很好地翻译成英语。
paercebal

至于“不同部分”,如其标题所示,对不起,我想这篇文章的撰写时间相当长。至于粗体字,是其所在段落的摘要。只需要信息而没有理由的人可以限制他们对标题和粗体字的阅读。
paercebal

3
+1。真的很好的解释。我们正在等待这一点的实施。对我们来说真的很重要。我们致力于MDE(模型驱动工程),并认为这是必要的。我将在下面添加一个回复以说明我们的情况。
圣地亚哥塞维利亚

9
@TGV::you can write 1+2i, but you still can't write a+bi, so there's absolutely no point即使忽略您的a+bi示例也是荒谬的,但您将其视为“低频”这一事实并不意味着每个人都会这样做。。。纵观全局,重点是要确保用户定义的对象以及内置类型尽可能多地被视为该语言的一等公民。因此,如果您可以写1.5fand 1000UL,为什么不写25i,甚至不能写100101b呢?与C和Java相反,用户类型不应视为C ++语言的二等公民。
paercebal 2011年

3
@Anton ::Most of data still comes from IO代码中有很多硬编码值。查看代码中出现的所有布尔值,所有整数和所有双精度数,因为写起来x = 2 * y ;x = Two * y在哪里Two强类型常量更方便。用户定义的文字使我们能够在其上放置类型并编写:x = 2_speed * y ;并使编译器验证计算是否合理。。。这一切都与强类型有关。。。也许您不会使用它。但我肯定会,只要能够在工作中使用启用C ++ 11的编译器。
paercebal

36

对于数学代码来说非常好。我不知道可以看到以下运算符的用法:

度为度。这使得编写绝对角度更加直观。

double operator ""_deg(long double d)
{ 
    // returns radians
    return d*M_PI/180; 
}

它也可以用于各种定点表示(在DSP和图形领域仍在使用)。

int operator ""_fix(long double d)
{ 
    // returns d as a 1.15.16 fixed point number
    return (int)(d*65536.0f); 
}

这些看起来像很好的示例,说明如何使用它。它们有助于使代码中的常量更具可读性。它也是使代码不可读的另一种工具,但是我们已经有太多的工具滥用,以至于再伤害也不多。


1
“但是,我们已经有太多的工具滥用,以至于再没有多大的伤害。 ”哇,我希望这不是最近c ++ [x] 1234567890功能泛滥的全部原理。想象一下,必须学习一个使用所有这些库的库,再加上十种文件格式进行配置以及两个工具对代码进行预处理和后处理...
masterxilo 2014年

@masterxilo当然,实际上,您的示例是荒谬的:UDL并不比函数更难学习,因为它们只是语法糖-而且,任何好的lib都仅使用改进UX所需的功能-并准确记录其功能一切手段。如果有人过度使用某个功能来生成无法读取的代码(假设这在他们的工作中是完全可以避免的...),那么这并不会导致该功能的过错,而只是使用上的错误。另外,一个人的不可读就是另一个人的头疼。都是意见-和选项。如果您不喜欢它们,请不要担心!你不具备使用它们。其他人可以
underscore_d

17

UDL具有命名空间(可以通过使用声明/指令导入,但不能显式命名诸如3.14std::i)的文字,这意味着(希望)不会有很多冲突。

它们实际上可以被模板化(和constexpr'd),这意味着您可以使用UDL做一些非常强大的工作。Bigint作者将非常高兴,因为他们最终可以在编译时(通过constexpr或模板)计算出任意大的常量。

我只是难过,我们将不会看到标准(从外观上来看)一对夫妇有用的文字,喜欢sstd::stringi为虚数单位。

UDL可以节省的编码时间实际上并不是很高,但是可读性将大大提高,并且可以将越来越多的计算转移到编译时以加快执行速度。


感谢您澄清有关命名空间的观点...我想知道。
内森·里德

12

让我补充一点背景。对于我们的工作,非常需要用户定义的文字。我们致力于MDE(模型驱动工程)。我们想用C ++定义模型和元模型。实际上,我们实现了从Ecore到C ++(EMF4CPP)的映射。

当能够将模型元素定义为C ++中的类时,就会出现问题。我们正在采用将元模型(Ecore)转换为带有参数的模板的方法。模板的参数是类型和类的结构特征。例如,具有两个int属性的类将类似于:

typedef ::ecore::Class< Attribute<int>, Attribute<int> > MyClass;

但是,事实证明,模型或元模型中的每个元素通常都有一个名称。我们想写:

typedef ::ecore::Class< "MyClass", Attribute< "x", int>, Attribute<"y", int> > MyClass;

但BUT,C ++或C ++ 0x均不允许这样做,因为字符串被禁止用作模板的参数。您可以通过char来写名字char,但这确实是一团糟。使用正确的用户定义文字,我们可以编写类似的内容。假设我们使用“ _n”来标识模型元素名称(我不使用确切的语法,只是想出一个主意):

typedef ::ecore::Class< MyClass_n, Attribute< x_n, int>, Attribute<y_n, int> > MyClass;

最后,将这些定义作为模板可以帮助我们设计很多真正有效的遍历模型元素,模型转换等的算法,因为类型信息,标识,转换等是由编译器在编译时确定的。


4
非常喜欢这个by the compiler at compile time部分... :-)
paercebal 2012年

12

Bjarne Stroustrup在此C ++ 11演讲中,在有关类型丰富的接口的第一部分中谈到UDL ,大约20分钟。

他对UDL的基本论点是三段论:

  1. “临时”类型(即内置的原始类型)只能捕获琐碎的类型错误。类型丰富的接口使类型系统可以捕获更多类型的错误。

  2. 类型丰富的代码可能会捕获的类型错误会影响实际代码。(他举了一个“火星气候轨道器”的例子,该火星轨道因一个重要常数的尺寸误差而臭名昭著)。

  3. 在实际代码中,很少使用单位。人们不使用它们,因为创建丰富类型的运行时计算或内存开销太高了,而且使用预先存在的C ++模板化单元代码在概念上非常丑陋,以至没有人使用它。(根据经验,即使库已经存在了十年,也没有人使用它)。

  4. 因此,为了使工程师能够在真实代码中使用单元,我们需要一种设备(1)不会产生运行时开销,并且(2)在符号上是可接受的。


9

支持编译时维度检查是唯一需要的理由。

auto force = 2_N; 
auto dx = 2_m; 
auto energy = force * dx; 

assert(energy == 4_J); 

例如,请参阅PhysUnits-CT-Cpp11,这是一个小的C ++ 11,C ++ 14仅限标头的库,用于进行编译时维分析以及单位/数量操作和转换。比Boost.Units更简单,它确实支持单位符号文字(例如m,g,s),度量标准前缀(例如m,k,M),仅取决于标准C ++库,仅SI,尺寸的整数幂。


或查看units,这是一个基于c ++ 14构建的编译时,仅标头的维分析和单位转换库,Nic Holthaus没有依赖性。
马丁·莫恩

6

嗯...我还没考虑过这个功能。您的样本经过深思熟虑,肯定很有趣。C ++现在非常强大,但不幸的是,您阅读的代码片段中使用的语法有时过于复杂。如果不是全部,那么可读性至少是很多。这样的功能将为提高可读性做好准备。如果我举最后一个例子

assert(1_kg == 2.2_lb); // give or take 0.00462262 pounds

...我想知道您今天如何表达这一点。您将拥有KG和LB类,并且将比较隐式对象:

assert(KG(1.0f) == LB(2.2f));

那也一样。对于具有较长名称的类型,或者您不希望拥有如此好的构造函数而无需编写适配器,那么对于即时隐式对象创建和初始化来说,它可能是不错的选择。另一方面,您也已经可以使用方法创建和初始化对象。

但是我同意尼尔斯在数学上的观点。例如,C和C ++三角函数需要以弧度为单位的输入。我认为,以度为单位,因此像Nils这样的简短隐式转换非常好。

最终,它将成为语法糖,但是它将对可读性产生轻微影响。而且可能也更容易写一些表达式(sin(180.0deg)比sin(deg(180.0))更容易编写。然后会有人滥用这个概念。非常严格的语言,而不是像C ++那样具有表现力的语言。

嗯,我的帖子基本上什么也没说,除了:没关系,影响不会太大。不用担心 :-)


5
您的括号不平衡!抱歉,我的OCD也讨厌我。
X-Istence

3

我从不需要或不需要此功能(但这可能是Blub效果)。我的下意识反应是它很la脚,并且可能吸引那些认为对于任何可以远程解释为添加的操作重载operator +都是很酷的人。


我确认:非常有趣的文章。
paercebal

2

C ++通常对所使用的语法非常严格-除非预处理器太多,否则可以用来定义自定义语法/语法。例如,我们可以重载现有的歌剧,但不能定义新的歌剧-IMO,这与C ++的精神非常吻合。

我不介意通过某些方式来定制更多的源代码-但是选择的要点对我来说似乎很孤立,这使我最困惑。

即使是预期的用途,也可能使阅读源代码变得更加困难:单个字母可能会产生影响深远的副作用,而这些副作用绝对无法从上下文中识别出来。由于对称于u,l和f,大多数开发人员将选择单个字母。

这也可能使范围界定成为一个问题,在全局名称空间中使用单个字母可能被认为是不好的做法,被认为更容易混合库的工具(命名空间和描述性标识符)可能会破坏其目的。

我看到与“自动”结合使用还有一些优点,也与诸如Boost单位之类的单位库结合使用,但不足以胜任这一工作。

但是,我想知道我们提出了什么聪明的主意。


1
using single letters in global namespace will probably be considered bad practice但这无关紧要:(A)必须在(非全局)命名空间范围内定义UDL ...大概是因为(B)它们必须由一个下划线然后大于等于1的字母组成,而不仅仅是字母,以及此类标识符全局NS保留用于实现。这与UDL本质上会引起混淆的观点至少有2分。至于必须限制名称空间的范围,以减少功能的实用性,这就是为什么例如stdlib在中将它们声明为,inline namespace以便用户可以根据需要导入批发。
underscore_d

2

我将用户文字用于二进制字符串,如下所示:

 "asd\0\0\0\1"_b

使用std::string(str, n)构造函数,\0这样就不会将字符串切成两半。(该项目对各种文件格式做了很多工作。)

当我不愿std::string为包装使用包装纸时,这也很有帮助std::vector


-5

那东西的线路噪音很大。读起来也很恐怖。

让我知道,他们是否有理由通过任何示例添加新的语法?例如,他们有几个已经使用C ++ 0x的程序吗?

对我来说,这部分内容:

auto val = 3.14_i

没有理由说明这一部分:

std::complex<double> operator ""_i(long double d) // cooked form
{ 
    return std::complex(0, d);
}

即使您也要在其他1000行中使用i语法也是如此。如果您写的话,您可能还会写10000行其他内容。尤其是当您仍然可能会在几乎所有地方都这样编写时:

std::complex<double> val = 3.14i

不过,“ auto”-关键字可能是合理的,也许只有这样。但是让我们仅使用C ++,因为在这方面它比C ++ 0x好。

std::complex<double> val = std::complex(0, 3.14);

就像..就这么简单。甚至认为如果在任何地方使用它,所有的std和尖括号都只是la脚。我不会开始猜测C ++ 0x中有什么语法可以将std :: complex转换为complex。

complex = std::complex<double>;

也许这很简单,但是我不相信在C ++ 0x中就这么简单。

typedef std::complex<double> complex;

complex val = std::complex(0, 3.14);

也许?> :)

无论如何,关键是:编写3.14i而不是std :: complex(0,3.14); 除了极少数特殊情况外,总体上不会为您节省很多时间。


10
@Cheery:对您来说,“ auto val = 3.14i”并不能证明为支持该代码而编写的代码。我可以回答这个问题,对我来说“ printf(”%i“,25)”不能证明为printf编写的代码是合理的。你看到图案了吗?
paercebal

5
@Cheery:“那东西的线路噪音很大”。不,不是……“而且阅读太可怕了”。您的主观论点很有趣,但是您通常应该看一下运算符重载,以了解该功能的代码并不令人惊讶/震惊...对于C ++开发人员
paercebal

3
自动将有助于提高可读性。考虑在for循环中使用插入器:for(auto it = vec.begin(); it!= vec.end(); ++ it)...我了解for_each,但不喜欢必须创建函子来使用它。
KitsuneYMG,2009年

1
@kts:用C ++ 1X我们将有Lambda和范围
乔d

3
如果您的C ++行比C ++ 0x好,那么我的行就更好了。只写:std::complex<double> val(0, 3.14);
Ben Voigt
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.