Left-to-Right vs. Right-to-Left Coding Styles

科技   2024-04-18 08:36   浙江  

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

前言

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 = {12345};
2
3std::vector<int>::iterator it;
4for (it = numbers.begin(); it != numbers.end(); ++it) {
5    std::cout << *it << " ";
6}

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

1std::vector<int> numbers = {12345};
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 Tclass U>
3decltype(a + bf(TaUb) {

4    return a + b;
5}

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

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

虽说可以满足,但表意不够直观。新形式t-to-Left 则可以这样写:

1template <class Tclass U>
2auto f(TaUb) -> decltype(a + b) {

3    return a + b;
4}

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

1template <class Tclass U>
2auto f(TaUb) {

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 Tstruct A { using X = typename T::X; };
4
5// normal return type
6template <class Ttypename T::f(typename A<T>::X);
7template <class Tvoid f(...);
8
9// trailing return type
10template <class Tauto g(typename A<T>::X) -> typename T::X;
11template <class Tvoid 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) 这些具有细微差异的特性,再结合左值右值等概念,若无一定经验,使用起来可能容易出错。

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

推荐阅读  点击标题可跳转

1、Reflection for C++26

2、C++之父反驳白宫,称拜登政府忽视了现代C++编程语言的优势

3、回调函数(callback)是什么?一文理解回调函数(callback)

CPP开发者
我们在 Github 维护着 9000+ star 的C语言/C++开发资源。日常分享 C语言 和 C++ 开发相关技术文章,每篇文章都经过精心筛选,一篇文章讲透一个知识点,让读者读有所获~
 最新文章