144

C++11引入了用户定义的文字,这将允许基于现有文字(int, hex, string, float)引入新的文字语法,以便任何类型都能够有文字表示。

例子:

// 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(作为plain ints),然后才评估(06_AD更不用说八月和九月必须在没有0八进制原因的情况下编写)。这可以通过使用语法来解决,1974-1/6_AD以便运算符评估顺序有效,但它很笨重。

所以我的问题归结为这个,你觉得这个功能会证明自己的合理性吗?您还想定义哪些其他文字以使您的 C++ 代码更具可读性?


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

4

12 回答 12

195

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

但是当深入研究时,我们发现它不仅仅是语法糖,因为它扩展了 C++ 用户的选项来创建行为与不同的内置类型完全相同的用户定义类型。在此,这个小小的“奖励”是对 C++ 的一个非常有趣的 C++11 补充。

我们真的需要在 C++ 中使用它吗?

我在过去几年编写的代码中几乎没有看到任何用途,但仅仅因为我没有在 C++ 中使用它并不意味着它对其他 C++ 开发人员没有兴趣。

我们曾在 C++(我猜也是 C)中使用编译器定义的文字,将整数输入为短整数或长整数,将实数输入为浮点数或双精度数(甚至长双精度数),将字符串输入为普通字符或宽字符.

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

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

所以,我想这是语言的自然演变,但要尽可能完整:“如果你想创建一个类型,并且希望它尽可能地像内置类型一样表现,这里有工具。 ……

我猜这与 .NET 将每个基元都设为结构(包括布尔值、整数等)并让所有结构派生自 Object 的决定非常相似。仅此一项决定就使 .NET 在处理原语时远远超出了 Java 的范围,无论 Java 将在其规范中添加多少装箱/拆箱 hack。

你真的需要在 C++ 中使用它吗?

这个问题是来回答的。不是 Bjarne Stroustrup。不是赫伯萨特。不是 C++ 标准委员会的任何成员。这就是为什么您可以在 C++ 中进行选择的原因,它们不会将有用的符号单独限制为内置类型。

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

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

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

臃肿和复杂(双关语)是有区别的。

就像 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 日期,但这取决于您的需要。这就是让用户定义自己的文字名称的全部意义所在。

于 2008-10-26T14:17:30.703 回答
72

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

#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(至少不能没有字符串模板参数)。

于 2011-10-26T17:41:55.843 回答
37

这对数学代码非常有用。在我看来,我可以看到以下运算符的用途:

deg 表示度数。这使得书写绝对角度更加直观。

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); 
}

这些看起来像是如何使用它的好例子。它们有助于使代码中的常量更具可读性。它也是使代码不可读的另一种工具,但我们已经有太多工具滥用,再多一个也不会造成太大伤害。

于 2008-10-26T10:16:31.733 回答
17

UDL 是命名空间的(并且可以通过使用声明/指令导入,但您不能显式命名一个像 一样的文字3.14std::i),这意味着(希望)不会有大量冲突。

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

我很遗憾我们不会在标准中看到几个有用的文字(从它的外观来看),比如sforstd::stringifor 虚数单位。

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

于 2008-10-26T18:20:03.473 回答
13

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

他对 UDL 的基本论证采用三段论的形式:

  1. “琐碎”类型,即内置的原始类型,只能捕获琐碎的类型错误。具有更丰富类型的接口允许类型系统捕获更多种类的错误。

  2. 富类型代码可以捕获的类型错误对实际代码有影响。(他举了火星气候轨道飞行器的例子,由于一个重要常数的尺寸错误而臭名昭著地失败了)。

  3. 在实际代码中,很少使用单位。人们不使用它们,因为产生丰富类型的运行时计算或内存开销过于昂贵,而且使用预先存在的 C++ 模板化单元代码在符号上是如此丑陋,以至于没有人使用它。(根据经验,没有人使用它,即使这些库已经存在了十年)。

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

于 2013-09-09T00:10:00.650 回答
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;

但是,C++ 和 C++0x 都不允许这样做,因为字符串被禁止作为模板的参数。您可以逐个字符地编写名称 char,但这无疑是一团糟。使用适当的用户定义文字,我们可以编写类似的东西。假设我们使用“_n”来识别模型元素名称(我不使用确切的语法,只是为了提出一个想法):

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

最后,将这些定义作为模板有助于我们设计用于遍历模型元素、模型转换等的算法,这些算法非常有效,因为类型信息、标识、转换等是由编译器在编译时确定的。

于 2010-10-29T11:19:42.753 回答
8

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

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、维度的整数幂。

于 2016-07-02T19:04:30.620 回答
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++ 那样富有表现力的语言。

啊,我的帖子基本上什么都没说,除了:会好的,影响不会太大。我们不要担心。:-)

于 2008-10-26T10:31:52.857 回答
3

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

于 2008-10-26T10:03:09.483 回答
2

C++ 通常对所使用的语法非常严格——除了预处理器之外,您没有太多可以用来定义自定义语法/语法的东西。例如,我们可以重载现有的操作符,但我们不能定义新的操作符——IMO 这非常符合 C++ 的精神。

我不介意更多定制源代码的一些方法 - 但是选择的点对我来说似乎很孤立,这让我最困惑。

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

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

我看到与“auto”相结合的一些优点,也与诸如boost units之类的单元库相结合,但不足以值得这个附加。

然而,我想知道我们想出了什么聪明的主意。

于 2008-10-26T19:44:30.433 回答
2

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

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

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

当我放弃std::string支持std::vector.

于 2015-08-27T13:24:22.490 回答
-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' -keyword 可能是合理的,只是也许。但是让我们只考虑 C++,因为在这方面它比 C++0x 更好。

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

就像……就这么简单。甚至认为如果您在任何地方都使用它,所有的标准括号和尖括号都是蹩脚的。我没有开始猜测 C++0x 中有什么语法可以将 std::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); 除了少数超级特殊情况外,总体上不会为您节省太多时间。

于 2008-10-26T11:14:53.317 回答