编码风格(二)——使用具有风格的语言特性、格式和风格的挑战
目录
- 1. 使用具有风格的语言特性‘
- 1.1 使用常量
- 1.2 使用引用代替指针
- 1.3 使用自定义异常
- 2. 格式
- 2.1 关于大括号对齐的争论
- 2.2 关于空格和圆括号的争论
- 2.3 空格、制表符、换行符
- 3. 风格的挑战
- 参考
1. 使用具有风格的语言特性‘
C++语言允许执行各种非常难以读懂的操作。看下面的古怪代码:
i++ + ++i;
这行代码很难懂,但更重要的是,C++标准没有定义它的行为。问题在于i++使用了i的值,还是递增了i的值。C++标准没有说明什么时候递增其值,这个副作用(递增)只有在“;”之后才能看到,但是编译器执行到这一行时,可以在任意点执行递增。无法知道哪个i值会用于++i部分,在不同的编译器和平台上执行这行代码,会得到不同的值。
如下的示例表达式:
a[i] = ++i;
在C++17中,它的行为是确定的,在对赋值运算的左侧求值之前会保证完成对右侧的所有操作的求值。所以,在本例中,i首先递增,然后在a[i]中用作索引。即使如此,为了清楚起见,仍然建议避免使用此类表达式。
在使用C++语言提供的强大功能时,一定要考虑如何以良好的风格使用语言特性。
1.1 使用常量
不良代码经常乱用“魔法数字”。在一些函数中,代码可能使用2.71828、24或3600等。为什么呢?这些值有什么含义?具有数学背景的人会发现,这代表e的近似值,但多数人不知道这一点。C++语言提供了常量,可以把一个符号名称赋予某个不变的值,例如2.71828、24、3600等,下面是几个示例:
const double ApproximationForE { 2.71828182845904523536 };
const int HoursPerDay { 24 };
const int SecondsPerHour { 3'600 };
注意:从C++20开始,标准库包含一组预定义的数学常量,所有这些常量都定义在std::numbers名称空间的<numbers>中。例如,它定义了std::numbers::e、pi、sqrt2、phi等。
1.2 使用引用代替指针
以前,C++程序员通常开始学的是C。在C中,指针是按引用传递的唯一机制,多年来一直运行良好。在某些情况下仍然需要指针,但在许多情况下可以用引用代替指针。如果开始学习的是C,可能认为引用实际上没有给C++语言增加新的功能,只是引入了一种新的语法,其功能已经由指针提供。
用引用替换指针有许多好处。首先,引用比指针安全,因为引用不会直接处理内存地址,也不会是nullptr。其次,引用在风格上比指针好,因为引用使用与栈上变量相同的语法,没有*和&等符号。引用易于使用,因此将引用加入编码风格库中没有任何问题。遗憾的是,某些程序员认为,如果在函数调用中看到&,被调用的函数将改变对象;如果没有看到&,对象一定是按值传递。而使用引用,就无法判断函数是否将改变对象,除非看到函数原型。这种思维方式是错误的。用指针传递未必意味着对象将改变,因为参数可能是const T*。传递指针或引用是否会修改对象,都取决于函数原型是否使用了const T*、T*、const T&或T&。因此,只有查看函数原型,才能判断函数是否修改对象。
使用引用的另一个好处是它明确了内存的所有权。如果你编写了一个方法,另一个程序员传递给它一个对象的引用,很明显可以读取并修改这个对象,但是无法轻易地释放对象的内存。如果传递的是一个指针,就不那么明显。需要删除对象来清理内存吗?还是调用者需要这样做?在现代C++中,处理内存所有权和转让所有权的较好方法是使用后面介绍的智能指针。
1.3 使用自定义异常
C++可以很方便地忽略异常。这一语言的语法没有强制处理异常,理论上,可以很方便地使用传统的机制,例如返回特殊值(如-1或nullptr),或者设置错误标志,来编写容错程序。当返回特殊值来处理错误时,可以使用[[nodiscard]]属性强迫函数的调用者处理返回值。
异常提供了更丰富的错误处理机制,自定义异常允许根据需要进行处理。例如,Web浏览器的自定义异常类型可以包括指定包括错误的网页、发生错误时的网络状态以及其他上下文信息的字段。后面会详细介绍异常。
2. 格式
2.1 关于大括号对齐的争论
或许被议论最多的是在哪里使用界定代码块的大括号。大括号的使用有多种格式,其中一种格式是除了类、函数和方法名之外,大括号与起始语句放在同一行。下面的代码显示了这种格式:
void someFunction()
{if ( condition() ) {std::cout << "condition was true\n";} else {std::cout << "condition was false\n";}
}
这种格式节省了垂直空间,同时仍然通过缩进显示代码块。有些程序员认为,节省垂直空间与现实世界的编码无关。下面显示了一段冗长的代码:
void someFunction()
{if ( condition() ){std::cout << "condition was true\n";}else{std::cout << "condition was false\n";}
}
有些程序员更自由地使用水平空间,编写的代码如下:
void someFunction()
{if ( condition() ){std::cout << "condition was true\n";}else{std::cout << "condition was false\n";}
}
另一个争论点在于,是否在一条语句周围放置大括号,例如:
void someFunction()
{if ( condition() )std::cout << "condition was true\n";elsestd::cout << "condition was false\n";
}
当然,不会推荐任何特定的格式。即使是单个语句,建议使用大括号,因为它可以比避免某些写得不好的C风格的宏带来的危害,并且在将来添加语句时更安全。
注意:当选择表示代码块的风格时,最重要的事情是应该能够让读者一眼就看出某个代码块对应的条件。
2.2 关于空格和圆括号的争论
单行代码的格式也能够引起争论。建议在任何关键字之后使用空格,在运算符前后都使用空格,在参数列表或函数调用中的每个逗号之后都使用空格,并使用圆括号表明操作顺序,如下所示:
if ( i == 2 ) {j = i + ( k / m );
}
另一种格式在关键字和左括号之间没有空格,如下所示。另外,if语句内用于明确操作顺序的圆括号也被省略了,因为它们没有语义相关性。
if( i == 2 ) {j = i + k / m;
}
区别十分微妙,请自行判断那种方法更好。
2.3 空格、制表符、换行符
空格和制表符的使用并不只是风格上的偏好。如果团队没有使用空格和制表符的约定,当程序员一起工作时会出大问题。最明显的问题是:Alice使用4个空格的制表符缩进代码,而Bob使用5个空格的制表符。当他们使用同一文件时,将无法正确显示代码。如果Bob用制表符重新整理代码格式,同时Alice编辑同样的代码,情况更糟糕,许多源代码控制系统不能合并Alice所做的修改。
大多数编辑器可设置空格和制表符。某些环境甚至在读取代码时会调整代码的格式,或者即使编写代码时用的是制表符,保存时也总是使用空格。如果环境比较灵活,使用他人的代码会更容易。记住,制表符和空格是不同的,因为制表符的长度不定,而空格始终是空格。
最后,并非所有平台都以相同的方式表示换行。例如,Windows使用\r\n作为换行符,而基于Linux的平台通常使用\n。如果你在公司中使用多个平台,那么需要就使用那种换行样式达成一致。同样,IDE很可能被配置为使用需要的换行符样式,或者可以使用自动化工具来自动修复换行符,例如,在将代码提交到源代码控制系统时。
3. 风格的挑战
许多程序员在项目开始时都保证他们做好每件事。只要变量或参数永远不变,就将其标记为const。所有变量都具有清楚的、简明的、容易阅读的名称。每个开发人员都将左大括号放在后续行,采用标准文本编辑器,并遵循关于制表符和空格的约定。
维持这种层次的格式一致非常困难,原因有很多。当涉及const时,有些程序员不知道如何用它。总会遇到不支持const的旧代码或库函数。例如,假设你正在编写一个接收const参数的函数,并且需要调用一个接收非const参数的遗留函数。如果你无法修改遗留代码使其兼容const,可能是因为它是第三方库,并且你绝对确定遗留函数不会修改其非const参数,经验丰富的程序员会使用const_cast暂时取消变量的const属性,但缺少经验的程序员会取消来自调用函数的const属性,导致程序从不使用const。
有时,标准化的格式会与程序员的个人口味和偏好发生冲突。或许团队文化无法强制使用严格的风格准则。此类情况下,必须判断哪些元素需要标准化,哪些元素可以由个人决定其风格。甚至可以获取或编写脚本,自动纠正格式“bug”,或将格式问题与代码错误一起标记。一些开发环境,例如Microsoft Visual C++,支持根据指定的规则自动格式化代码,这样就很容易编写出始终遵循指定规则的代码。
参考
[比] 马克·格雷戈勒著 程序喵大人 惠惠 墨梵 译 C++20高级编程(第五版)