前言
Classic C++ 中,声明风格是自右向左,如:
1int f() {}
2int a = 42;
3std::string s{"str"};
而 Modern C++ 中变为自左向右,对应写法为:
1auto f() -> int {}
2auto a = 42;
3auto s = std::string{"str"};
其实很多编程语言都采用或支持 Left-to-Right 这种声明风格,下面列举几种。
Rust:
1fn f(num: i32) -> i32 {}
2let x = f(5);
Swift:
1func f(_ n: Int) -> Int {}
2let x = f(5)
Python:
1# type hinting
2def f(num: int) -> int:
3 return num * num
4
5x = f(5)
Haskell:
1f :: Int -> Int
2f n = n * n
3
4x :: Int
5x = f 5
这里只列举了一些同样使用 ->
表示返回类型的语言,它们都基于数学中对函数的表示 f: X -> Y
。因此,如果你是来自于这一系的 C++ 学习者,Left-to-Right 的这种新形式也许会更加友好。
从视觉上来说,代码本身就是左对齐,采用 Left-to-Right 这种语法形式能够使代码风格更具有一致性,且可突出重点,缺点则是声明更长。
1// Classic C++
2struct S {
3 int f1() {}
4 char f2() {}
5 std::string f3() {}
6 SomeOtherTypeWithALongName f4() {}
7};
8int a = 42; // variable
9char func(); // function
10
11// Modern C++
12struct S {
13 auto f1() -> int {}
14 auto f2() -> char {}
15 auto f3() -> std::string {}
16 auto f4() -> SomeOtherTypeWithALongName {}
17};
18
19auto a = 42; // variable
20auto func() -> char; // function
这些只是形式上的差异,算不得主要,下面来看其他差异。
细数差异
1. 繁杂名称
许多时候,你可能并不关心某些类型名称,典型例子是迭代器。
1std::vector<int> numbers = {1, 2, 3, 4, 5};
2
3std::vector<int>::iterator it;
4for (it = numbers.begin(); it != numbers.end(); ++it) {
5 std::cout << *it << " ";
6}
旧式写法过于繁琐,损耗精力,新式写法具有推导能力,可以直接用省略名称。
1std::vector<int> numbers = {1, 2, 3, 4, 5};
2
3for (auto it = numbers.begin(); it != numbers.end(); ++it) {
4 std::cout << *it << " ";
5}
当然如今这种写法也略显冗余,举例而已。
同样的名称,还包含 Literal suffixes,Smart Pointers 等等,皆属此类,不胜枚举。
2. 名称查找
编译器从左向右扫描并解析名称,使用 Right-to-Left 这种形式某些情况下必须写出完整的名称,才能让名称查找正常工作。
一个例子:
1struct DummyName {
2 using type = std::vector<int>;
3 type f();
4};
5
6DummyName::type DummyName::f() {
7 return {};
8}
你必须完整地写出 DummyName::type
,而不是 type
,否则名称查找失败。而 Left-to-Right 无此约束:
1struct DummyName {
2 using type = std::vector<int>;
3 type f();
4};
5
6auto DummyName::f() -> type {
7 return {};
8}
这将减少不少重复。不明白原因请再次翻开 洞悉函数重载决议,查看 Name Lookup 小节。
3. 泛型代码
同样由于名称解析顺序,以下这种情况 Right-to-Left 无能为力:
1// Error!
2template <class T, class U>
3decltype(a + b) f(T& a, U& b) {
4 return a + b;
5}
函数需要返回两个类型相加产生的新类型,但因为解析顺序,编译器在遇到 a,b
之前无法识别名称。当然,可以借助 std::common_type
来满足需求:
1template <class T, class U>
2std::common_type_t<T, U> f(T& a, U& b) {
3 return a + b;
4}
虽说可以满足,但表意不够直观。新形式t-to-Left 则可以这样写:
1template <class T, class U>
2auto f(T& a, U& b) -> decltype(a + b) {
3 return a + b;
4}
更简单的方式是采用 C++14 的自动类型推导,代码最少:
1template <class T, class U>
2auto f(T& a, U& b) {
3 return a + b;
4}
但这种方式声明和实现都必须放在 .h
里面。
4. 复杂声明
有些声明非常复杂,比如:
1void (*f(int i))(int);
尽管可以分析出 f
的实际类型为:
f is a function passing an int returing a pointer to a function passing an int returning void.
但是可阅读性很差,而采用 Left-to-Right 可使表意一目了然。
1auto f(int i) -> void (*)(int);
5. 修饰位置
一个不同地方在于,override
的修饰位置不同。
1struct Base {
2 virtual int f() const noexcept;
3};
4
5struct Derived: Base {
6 virtual int f() const noexcept override;
7};
Right-to-Left 风格的 override
和其他修饰符靠得很近,而 Left-to-Right 则略显奇怪:
1struct Base {
2 virtual auto f() const noexcept -> int;
3};
4
5struct Derived: Base {
6 virtual auto f() const noexcept -> int override;
7};
override
与其他修饰符位置距离甚远,始终出现在声明结尾。
6. SFINAE Friendly
另一个细微的差异,看 The Book of Modern C++ §1.3.2 中详细解释过的一个例子。
1// Example from ISO C++
2
3template <class T> struct A { using X = typename T::X; };
4
5// normal return type
6template <class T> typename T::X f(typename A<T>::X);
7template <class T> void f(...);
8
9// trailing return type
10template <class T> auto g(typename A<T>::X) -> typename T::X;
11template <class T> void g(...);
12
13int main() {
14 f<int>(0); // #1 OK
15 g<int>(0); // #2 Error
16}
这两种写法完全相同,但是此处 Right-to-Left 版本将产生 SFINAE,而 Left-to-Right 版本则会产生 Hard-error。前者是 SFINAE-Friendly,而后者并不是,是以会编译失败。
7. Lambdas
Lambda expressions 类型为 closure type,没有办法显式写出类型,此时必须采用 Left-to-Right 的写法。
1auto f = [](int a) -> int {
2 return a * a;
3}
这个是最无法替代的一类,因此不论是否喜欢新风格,其实都会在某些情况下使用。
Conclusion
Left-to-Right 还是 Right-to-Left 好?这种争论毫无意义,风格所好本就是非常主观的事情,只要你觉得有使用的理由,大可以坚持自己的风格。
此外,Left-to-Right 中包含 auto, trailing-return-type, decltype(auto) 这些具有细微差异的特性,再结合左值右值等概念,若无一定经验,使用起来可能容易出错。
若是新项目,再考虑选择新的风格,旧项目就保持一致吧。