进入 Modern C++,声明风格由 Right-to-Left 逐渐转变为 Left-to-Right,个中差异,优劣得失,且看本篇内容。

前言

Classic C++ 中,声明风格是自右向左,如:

int f() {}
int a = 42;
std::string s{"str"};

而 Modern C++ 中变为自左向右,对应写法为:

auto f() -> int {}
auto a = 42;
auto s = std::string{"str"};

其实很多编程语言都采用或支持 Left-to-Right 这种声明风格,下面列举几种。

Rust:

fn f(num: i32) -> i32 {}
let x = f(5);

Swift:

func f(_ n: Int) -> Int {}
let x = f(5)

Python:

# type hinting
def f(num: int) -> int:
    return num * num

x = f(5)

Haskell:

f :: Int -> Int
f n = n * n

x :: Int
x = f 5

这里只列举了一些同样使用 -> 表示返回类型的语言,它们都基于数学中对函数的表示 f: X -> Y。因此,如果你是来自于这一系的 C++ 学习者,Left-to-Right 的这种新形式可能会更加友好。

从视觉上来说,代码本身就是左对齐,采用 Left-to-Right 这种语法形式能够使代码风格更具有一致性,且可突出重点,缺点则是声明更长。

// Classic C++
struct S {
    int f1() {}
    char f2() {}
    std::string f3() {}
    SomeOtherTypeWithALongName f4() {}
};
int a = 42;  // variable
char func(); // function

// Modern C++
struct S {
    auto f1() -> int {}
    auto f2() -> char {}
    auto f3() -> std::string {}
    auto f4() -> SomeOtherTypeWithALongName {}
};

auto a = 42; // variable
auto func() -> char; // function

这些只是形式上的差异,算不得主要,下面来看其他差异。

细数差异

1. 繁杂名称

许多时候,你可能并不关心某些类型名称,典型例子是迭代器。

std::vector<int> numbers = {1, 2, 3, 4, 5};

std::vector<int>::iterator it;
for (it = numbers.begin(); it != numbers.end(); ++it) {
    std::cout << *it << " ";
}

旧式写法过于繁琐,损耗精力,新式写法具有推导能力,可以直接用省略名称。

std::vector<int> numbers = {1, 2, 3, 4, 5};

for (auto it = numbers.begin(); it != numbers.end(); ++it) {
    std::cout << *it << " ";
}

当然如今这种写法也略显冗余,举例而已。

同样的名称,还包含 Literal suffixes,Smart Pointers 等等,皆属此类,不胜枚举。

2. 名称查找

编译器从左向右解析名称,使用 Right-to-Left 这种形式某些情况下必须写出完整的名称,才能让名称查找正常工作。

一个例子:

struct DummyName {
    using type = std::vector<int>;
    type f();
};

DummyName::type DummyName::f() {
    return {};
}

你必须完整地写出 DummyName::type,而不是 type,否则名称查找失败。Left-to-Right 则无此约束:

struct DummyName {
    using type = std::vector<int>;
    type f();
};

auto DummyName::f() -> type {
    return {};
}

这将减少不少重复。不明白原因请再次翻开 洞悉函数重载决议,查看 Name Lookup 小节。

3. 泛型代码

同样由于名称解析的顺序,下面这种情况,Right-to-Left 无能为力:

// Error!
template <class T, class U>
decltype(a + b) f(T& a, U& b) {
    return a + b;
}

函数需要返回两个类型相加的新类型,但因为解析顺序,编译器在遇到 a, b 之前无法识别名称。当然,可以借助 std::common_type 来满足需求:

template <class T, class U>
std::common_type_t<T, U> f(T& a, U& b) {
    return a + b;
}

虽说可以满足,但表意不够直观。Left-to-Right 可以直接这样写:

template <class T, class U>
auto f(T& a, U& b) -> decltype(a + b) {
    return a + b;
}

更简单的方式是采用 C++14 的自动类型推导,代码最少:

template <class T, class U>
auto f(T& a, U& b) {
    return a + b;
}

但这种方式声明和实现都必须放在 .h 里面。

4. 复杂声明

有些声明非常复杂,比如:

void (*f(int i))(int);

尽管可以分析出 f 的实际类型为:

f is a function passing an int returing a pointer to a function passing int returning void.

但是可阅读性很差,采用 Left-to-Right 可使表意一目了然。

auto f(int i) -> void (*)(int);

5. 修饰位置

一个不同地方在于,override 的修饰位置不同。

struct Base {
    virtual int f() const noexcept;
};

struct Derived: Base {
    virtual int f() const noexcept override;
};

Right-to-Left 风格的 override 和其他修饰符靠得很近,而 Left-to-Right 则略显奇怪:

struct Base {
    virtual auto f() const noexcept -> int;
};

struct Derived: Base {
    virtual auto f() const noexcept -> int override;
};

override 与其他修饰符位置相距甚远,始终出现在声明结尾。

6. SFINAE Friendly

另一个细微的差异,看 The Book of Modern C++ §1.3.2 中详细解释过的一个例子。

// Example from ISO C++

template <class T> struct A { using X = typename T::X; };

// normal return type
template <class T> typename T::X f(typename A<T>::X);
template <class T> void f(...);

// trailing return type
template <class T> auto g(typename A<T>::X) -> typename T::X;
template <class T> void g(...);

int main() {
    f<int>(0); // #1 OK
    g<int>(0); // #2 Error
}

这两种写法完全相同,但是此处 Right-to-Left 将产生 SFINAE,而 Left-to-Right 则会产生 Hard-error。前者是 SFINAE-Friendly,而后者并不是,是以编译失败。

7. Lambdas

Lambda expressions 的类型是 closure type,没有办法显式写出类型,此时必须采用 Left-to-Right 的写法。

auto f = [](int a) -> int {
    return a * a;
}

这个是最无法替代的一类,因此不论是否喜欢新风格,其实都会在某些情况下使用。

Conclusion

Left-to-Right 还是 Right-to-Left 好?这种争论毫无意义,风格所好本就是非常主观的事情,只要你觉得有使用的理由,大可以坚持自己的风格。

此外,Left-to-Right 中包含 auto, trailing-return-type, decltype(auto) 这些具有细微差异的特性,再结合左值右值等概念,若无一定经验,使用起来容易出错。

若是新项目,再考虑选择新的风格,旧项目就保持一致吧。

易出错的其他细微差异,不属本篇小主题,遂未涵盖,请待后续单独更新。若有遗漏,亦可补充。

Leave a Reply

Your email address will not be published. Required fields are marked *

You can use the Markdown in the comment form.