What new capabilities do user-defined literals add to C++?

C++11 introduces user-defined literals which will allow the introduction of new literal syntax based on existing literals (int, hex, string, float) so that any type will be able to have a literal presentation.

Examples:

// 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

At first glance this looks very cool but I'm wondering how applicable it really is, when I tried to think of having the suffixes _AD and _BC create dates I found that it's problematic due to operator order. 1974/01/06_AD would first evaluate 1974/01 (as plain ints) and only later the 06_AD (to say nothing of August and September having to be written without the 0 for octal reasons). This can be worked around by having the syntax be 1974-1/6_AD so that the operator evaluation order works but it's clunky.

So what my question boils down to is this, do you feel this feature will justify itself? What other literals would you like to define that will make your C++ code more readable?


Updated syntax to fit the final draft on June 2011

38442 次浏览

我从来没有需要或想要这个功能(但这可能是 布拉布效应)。我的下意识反应是,它很蹩脚,而且很可能会吸引那些认为操作员 + 超载很酷的人,因为任何操作都可能被远程解释为添加。

这对于数学代码来说是非常好的:

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

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

这些看起来像是很好的例子如何使用它。它们有助于使代码中的常量更易读。它也是另一个使代码不可读的工具,但是我们已经有太多的工具滥用,再多一个也没什么坏处。

嗯... 我还没有想过这个功能。你的样品经过深思熟虑,当然很有趣。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.0 deg)比 sin (180.0 deg)更容易写)。然后就会有人滥用这个概念。但是,语言滥用者应该使用非常严格的语言,而不是像 C + + 那样有表现力的语言。

啊,我的帖子基本上什么也没说,除了: 一切都会好起来的,影响不会太大。别担心。:-)

里面的线条噪音很大,而且读起来很恐怖。

让我知道,他们有没有用任何例子来推断新的语法添加?例如,他们有几个已经使用 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);

就好像。.就这么简单。即使所有的标准和尖括号都是蹩脚的,如果你在任何地方使用它。我不会开始猜测 C + + 0x 中有什么语法可以将 std: : 复杂性转换为复杂性。

complex = std::complex<double>;

这可能是一些简单的东西,但是我不相信它在 C + + 0x 中是那么简单的。

typedef std::complex<double> complex;


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

也许? > :)

无论如何,重点是: 写3.14 i 而不是 std: : plex (0,3.14) ; 除了在少数特殊情况下,总体上不会节省很多时间。

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

但是当我们深入研究的时候,我们发现它不仅仅是语法上的优势,就像 它扩展了 C + + 用户的选项,以创建行为与不同的内置类型完全一样的用户定义类型。一样。在这里,这个小小的“额外收获”是 C + + 11之外的一个非常有趣的补充。

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

我在过去几年编写的代码中看到了很少的用途,但是仅仅因为我没有在 C + + 中使用它,并不意味着它对于 另一个 C + + 开发人员不感兴趣。

我们在 C + + 中(我猜在 C 中也是)使用了编译器定义的字面值,将整数数字输入为短整数或长整数,将实数输入为 float 或 double (甚至长 double) ,将字符串输入为普通字符或宽字符。

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

让用户类型作为内置类型运行,我们仍然遗漏了一件事: 用户定义的文字。

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

我猜这和。NET 的决定,使每个基元成为一个结构,包括布尔值、整数等,并使所有的结构派生自 Object。光是这个决定。NET 在处理原语时远远超出了 Java 的能力范围,无论 Java 在其规范中添加多少装箱/拆箱技术。

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

这个问题需要 来回答。不是比雅尼·斯特劳斯特鲁普。不是 Herb Sutter。不是 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: : 复杂”类型都可以使用运算符重载进行乘、加、减等操作。

但是在 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 !

这样就很容易对值的赋值实施强类型化。

危险吗?

问得好。这些函数可以使用名称空间吗? 如果可以,那么 Jackpot!

无论如何,像所有事情一样,如果工具使用不当,你可能会自杀。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 日期,但它取决于您的需要。这就是让用户定义自己的文字名称的全部意义所在。

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

事实上,它们可以被模板化(以及 conexpr’d) ,这意味着您可以使用 UDL 完成一些非常强大的工作。Bigint 的作者会非常高兴,因为他们终于可以在编译时计算出任意大小的常量(通过 Constexpr 或模板)。

我只是感到遗憾的是,我们不会在标准中看到一些有用的文字(从外观上看) ,比如 s表示 std::stringi表示假想单位。

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

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

我不介意更多的自定义源代码的一些方法-但是所选择的观点对我来说似乎非常孤立,这使我最困惑。

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

这也可能导致作用域问题,在全局名称空间中使用单个字母可能会被认为是不好的做法,而那些被认为更容易混合库的工具(名称空间和描述性标识符)可能会达不到它的目的。

我看到一些优点结合“自动”,也结合单位库,如 启动装置,但不足以值得这个增加。

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

让我加点上下文。对于我们的工作,非常需要用户定义的文字。我们致力于 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;

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

这里有一个例子,使用用户定义的文字代替构造函数调用有一个优势:

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

在关于类型丰富的接口的第一部分,大约20分钟,比雅尼·斯特劳斯特鲁普在这个 C + + 11讲座中讨论了 UDL。

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

  1. “普通”类型,即内置基元类型,只能捕获普通类型错误。具有更丰富类型的接口允许类型系统捕获更多类型的错误。

  2. 富类型代码可以捕获的类型错误类型对实际代码有影响。(他举了一个火星气候探测者号的例子,由于一个重要常数的尺寸错误,这个方法臭名昭著地失败了。)。

  3. 在实际代码中,很少使用单位。人们不使用它们,因为创建富类型会产生运行时计算或内存开销,代价太高,而且使用预先存在的 C + + 模板化单元代码在表示法上非常丑陋,以至于没有人使用它。(实际上,没有人使用它,即使图书馆已经存在了十年)。

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

我对二进制字符串使用了用户字面值,如下所示:

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

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

当我放弃 std::string而选择 std::vector的包装器时,这也很有帮助。

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

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


assert(energy == 4_J);

参见例如 物理单位 -CT-Cpp11,一个小型的 C + + 11,C + + 14头文件库,用于编译时量纲分析和单位/数量操作和转换。比 推进,单位更简单,确实支持 单位符号文本,如 m,g,s,公制前缀,如 m,k,M,只依赖于标准的 C + + 库,SI-only,维度的整数幂。