C ++反模式

2021-01-22 13:02:46

预处理程序常数应始终使用大写字母。否则,您将得到最奇怪的错误,需要花费数小时至数天才能找到。假设您要在编译时打开/关闭某些功能。一种使用#ifdef的方法是这样的:

int make_query(Query const& q){//在此处进行一些查询。 #ifdef VERIFY_RESULT //执行一些验证。 #endif //其他一些任务和return语句。 }

编译时,您可以提供-DVERIFY_RESULT选项,然后预处理器将在验证代码中输入。在一个项目中,我已经看到了同样的事情,但是它与#ifdef verify_result有关。那是合法的C ++,并且与编译器的-Dverify_result命令行选项一起使用。

该项目的一个成员最近才加入,对许多编译标志一无所知,只是创建了一个新函数bool verify_result(Result const& result)。编译器没有说没有使用该名称。但是,相反,当给出该选项时,编译器看到了bool(结果const& result),因为该标志没有任何值。 GCC表示不希望(那里。

我不知道他们试图追踪这个问题有多长时间,但是它已经足够长,以至于成为一个真正的痛苦,当然也浪费了很多时间。如果预处理器常量一直都是大写的话,就不会发生这种情况。

另一个项目大量使用了预处理器标志来更改代码的工作方式。这本身不是问题。但是,有一些类似的代码:

有多个这样的标志,我看不到如何理解此代码。

通常将源文件包含在头文件中是错误的。一个优势案例是社区构建或模板专业化。所说的项目只是出于毫无原因的。

#if 0和#endif对可用于快速注释掉大块代码。与/ *和* /相反,它们具有可以被修饰的优点,并且还可以用作超级注释。

当周围有多个这些块时,很难理解为什么启用了某些而未启用的原因:

中间的是第一个的否定吗?第一个和第三个实际上是相同的吗?是否必须同时启用它们?

在这里,我更喜欢为此引入一些命名常量。以上内容可能如下所示:

这样就很容易理解发生了什么。如果有人想更改代码,则只需一个#define POLISH_WIDGETS就足够了,并且所有内容都是一致且可读的。

头文件应包含需要编译的所有其他头文件。在这里我见过几次:

请注意,此处未定义类型MyClass。它在此头文件中定义,但未包含在f.hpp中。

函数f的实现在f.cpp中。包含MyClass的标题:

由于编译器仅对.cpp文件起作用,因此实际上可以编译。给定#include语句的顺序,C ++编译器将看到以下内容:

只要在myclass.hpp之后始终包含头文件f.hpp,就可以使用该文件。当您在其他地方使用f.hpp时,它将开始失败。应该有一个#include" myclass.hpp"在f.hpp中

上面示例中f.cpp中头文件的顺序使隐藏f.hpp中缺少的include成为可能,从而仍可以编译。通过在.cppfiles中包含以下顺序,可以使此操作更早失败:

这样,标题中缺少include语句将变得直接可见。您可能会通过这种方式在第三方库中发现错误。

我看到了文件A.hpp提供类A1和A2的情况。文件B.hpp提供了B1。由于某些原因,依赖性为A1→B1→A2。这不是类型的直接循环依赖性,而是头文件的循环依赖性。这已经解决了。在文件A.hpp中的类似项目中:

当然,那编译就好了。但是同样,标头B.hpp不是独立标头,只能在此三明治中使用。我已经通过创建文件A1.hpp和A2.hpp并正确地将它们彼此包含在一起以打破文件的循环依赖性来解决了这一问题。

有两种截然不同的包含路径,当您使用#include<…>时会被搜索到。另一个用于#include"…"。前者用于全局安装的系统和第三方库头。您可以使用-I添加到该路径。后者适用于您的项目,可以用-i进行修改。不要将两者混合使用-I。为了将本地标头包含在#include<…>中。

可以争论使用命名空间的用法。在大多数情况下,我不会使用它来保持全局名称空间的整洁。使用命名空间std;在.cpp文件中可以,因为命名空间std只是转储在单个编译单元中。我不必担心其他文件中的内容。

一旦将使用命名空间std;的内容放入头文件中,它就会变成一个完全不同的故事。然后,包含它的所有其他头文件或源文件都将转储该名称空间。甚至使用该库的其他项目也将遭受尝试保存一些库程序类型的尝试。

我不喜欢#ifndef X,#define X,#endif样式包含后卫,因为它们很容易出错。他们很难阅读,但更糟糕的是,在一个项目中,他们两次都选择了包括卫兵。一个甚至可能具有与其他一些库已经使用过的相同的include防护。

如果是这样,错误将非常微妙。根据包含顺序,最后一个头文件将不包含在内。然后,您会得到有关未定义的类型和未定义的函数的错误,并且可能会在意识到问题发生之前拔掉头发。

除非您使用的是IBM XL 12,否则#pragma是一个不错的选择,但是该编译器无法执行C ++ 11,因此无论如何对我来说都不是那么有趣。

如果在bar.h中具有#ifndef FOO_H,则在创建另一个foo.h时会发生不好的事情。因此,包含保护应始终从路径派生,或仅使用#pragma。

在C ++中使用C函数可能是合理的做法。如果这样做,则不要包含X.h,而应包含cX。因此,不是< stdint.h>而是使用< cstdint>。这样C函数将正确地放在std名称空间中。

诸如printf,scanf,atoi,fopen,malloc和其他函数之类的函数不应在C ++中使用。有很多更好的方法。

由于我不知道的原因,在名称空间内有一些标准库头文件。我能想到的唯一原因是写:: std ::而不是std ::似乎工作太多。

GCC 6.0更改了标准标头,以便将它们包含在名称空间中时不起作用。这样一来,人们可以捕捉到这种反模式。

在C ++(和C)中,有64位无符号整数(uint64_t或无符号长整数)和指针(void *)。在机器中,它们是完全一样的东西,即64位无符号整数。对于类型系统,它仍然有很大的不同。

尽管它们只是计算机的数字,但应该可以帮助程序员阅读源代码并编写

这样一来,很明显一个是数字零,另一个是指针空。预处理器常量NULL仅是0或0u,因此编译器始终在其中看到纯0。仍然对程序员阅读很有帮助。在C ++ 11中,甚至存在nullptr,其类型正确,为nullptr_t。

出于某种原因,在一个项目中,指针被强制转换为整数。然后将它们转换为特殊的整数类型,您必须选择该整数作为标志进行配置,以使该整数的长度与指针的长度完全相同,否则编译器会给您带来错误。

我将其读为$ 2 ^ 3-1-$ 7。该值实际上是4,使用Clang编译时,它很方便地说:

Priority.cpp:7:25:警告:运算符'<'优先级低于'-&#39 ;; '-'首先将被评估[-Wshift-op-括号] uint32_t x = 1<< 3-1; ~~ ~~ ^ ~~ priority.cpp:7:25:注意:请在'-'周围加上括号。表达式以使此警告静音uint32_t x = 1<< 3-1; ^()

该行的原始作者到底是什么意思?他是否知道-操作的绑定强度要大于<<&lt ;?我可以不完全理解所有魔术数字的来源而信任该代码吗?

我见过一对夫妇用C ++语法编程Java。我的意思是说他们写了这样的东西:

他们没有再次处置分配的内存。如果他们这样做是为了分发内存或类似的东西,则需要进行堆分配。但是仅仅针对局部变量,这是胡说八道。我问过他为什么不写T t?并完成它。他说他习惯了T * t = new T();句法。然后我指出他有内存泄漏。据说运行时会照顾到这一点。太糟糕了,C ++中没有运行时...

从C ++ 11开始,有标准的std :: unique_ptr和str :: shared_ptr以及std :: make_shared和std :: make_unique(仅从C ++ 14开始),几乎可以消除所有需要指针的情况。

const是一件了不起的事情。尽管我是C ++中似乎盛行的最小注释概念的拥护者,但Rust的mut也许可以更好地解决这个问题。每当我用C ++编程时,我都会尝试将const放在我可以的任何地方。它可以帮助我推理代码(减少运动部件),并帮助编译器忽略一些变量。

如果您甚至没有为全局变量pi使用const,那简直就是麻烦。我的意思是,我只想在印第安纳州Pi法案中添加以下内容:

然后整个程序将改变其行为,调试起来很痛苦。

给这个魔术数字起个名字是个好习惯,但是为什么没有常量呢?

const关键字在那里是有原因的。遗憾的是它不是强制执行的C语言。但这就是为什么我们使用C ++的原因。我想到的项目几乎只包含全局变量和例程。人们甚至不能称它们为函数,它们仅具有副作用。

这不必要地增加了程序员必须同时处理自己的头的事情。这样的整体代码无法进行单元测试。您如何看待此类代码?

if(condition x){//某物} else {//其他if(condition x){//什么? }}

在某些情况下,您有多个线程并且使用了锁或原子变量。然后可能会到达此代码。否则,这只是膨胀。

使用别人测试过的代码很漂亮。因此,当您的编程语言的标准库支持类似缓冲区(std :: vectorperhaps)之类的东西时,请使用它!

一个项目包含其自己的缓冲区实现,该实现有一个严重的错误:调用clear()时,它不会将内部游标重置为开始并最终运行到其他内存中。这可能会破坏数据或(希望)使程序崩溃。只需使用标准库!

C ++具有static_cast,const_cast,dynamic_cast和reinterpret_cast。他们仅投射特定的属性,而不是全部。

然后很明显,它与类型有关(int与double),而不是与删除const或类似的东西有关。

有带符号(int,long,signed char)和无符号(unsigned int,unsigned long,char)整数类型。长期以来,我认为它们的主要区别在于存储负数的能力。但这不是它们的主要区别。类型的区别在于只有未签名的整数才定义了溢出行为。

大多数程序出于错误原因使用这些类型。使用无符号整数会强制函数的调用者给您一个正数。但是实际上,您是在(向编译器)指定要定义良好的溢出行为。大多数情况下,您不希望这样做,因此使用有符号整数实际上对于编译器而言是正确的选择。

请参阅Chandler Carruth的演讲,他在其中用具体的代码示例解释了该问题。

退出状态(main的返回值)向呼叫者发出错误信号。当从测试脚本中调用程序时,这一点很重要。没有适当的退出代码,就无法可靠地确定所调用程序的结果。

可能有" error"一词。在屏幕上供用户查看,但是调用脚本无法推断出问题。因此,使用exit(1)或其他一些非零数字向外部发出适当的信号。

我更想抛出一个异常。因此,我建议改为执行以下操作:

关键字throw在编辑器中脱颖而出,以表明该代码路径具有特殊意义。不需要" error"这个词大写字母,多个感叹号或类似符号。

$ ./a.outterminate在引发&st39 :: runtime_error'的实例后调用what():无法对小部件进行修饰!fish:' ./ a.out'由信号SIGABRT终止(Abbruch)

用户可以将程序加载到调试器中,并使用堆栈跟踪来查看错误的根源。对于一个简单的示例(在f中抛出异常),它看起来像这样:

(gdb)bt#0 0x00007ffff716d660从/lib64/libc.so。中提升(6)#1 0x00007ffff716ec41在中止()来自/lib64/libc.so.6中的#2 0x00007ffff7ae3025在__gnu_cxx :: __ verbose_terminate_handler()中从/ lib64 / libstdc ++。so.6#3 0x00007ffff7ae0c16在__cxxabiv1 :: __ terminate(void(*)())()来自/lib64/libstdc++.so.6#4 0x00007ffff7ae0c61 in std :: terminate()()来自/ lib64 / libstdc ++。so.6#5 0x00007ffff7ae0ea4在__cxa_throw()中来自/lib64/libstdc++.so.6#6 0x000000000040093a在f()中在cpp.cpp:7#7 0x0000000000400964在cpp中的主(argc = 1,argv = 0x7fffffffe588) .cpp:11

另外,我可以在堆栈中四处移动,并查看程序中的所有变量。

二进制数据格式比人类可读的简单文本文件更难理解。但是,将数字数组存储为二进制有很多优点:

文件大小就是内存中阵列的大小。这也意味着可以更有效地使用IO带宽。

无需解析文件,可以将其直接加载到内存中。

由于每个数字都具有完全相同的大小,因此可以并行读取和写入文件的块,例如使用MPI。

唯一的缺点是您可能很难更改架构。使用IEEE浮点,这对于所有普通计算机都应该是好的。有一些像PowerPC这样的体系结构可以在big endian中运行(尽管PowerPC两者都支持),而通常的x64 CPU在littleendian中可以工作。

如果您具有各种不同元素的复杂二进制格式,那么在没有实际程序的情况下可能很难读取数据。我建议不要这样做,而应使用HDF5之类的结构以二进制格式存储数据和元数据。

在项目中,我看到人们用简单的fprintf(fp,"%f&#34 ;, number)来写浮点数。起初看起来不错,因为您可以使用文本编辑器读取生成的文本文件。但是IO会比必要的慢一些,而且你只会得到几位数(默认是5位)。这意味着写入和读取数据将导致舍入错误。

即使我强烈建议为此使用二进制IO,在将数字写入文本文件时也至少要使用std :: numeric_limits< T> :: max_digits10(cppreferencepage)数字。这样,在从二进制到十进制再到十进制的转换中将是准确的。

一个类应具有许多不变量。这些是关于类表示的事物的一致性的声明。所有成员函数都需要将类从一种有效状态转移到另一种有效状态。在成员函数中,如果在函数结束之前恢复,则可能会暂时破坏不变式。

成员函数集应尽可能小。其他所有内容都应实现为自由功能。我经常在代码中看到很多便利函数塞满了班级的情况。

为了使用Metropolis算法实现Ising模型,需要使用某种2D数据结构。可以使用此类实现简单的二次晶格:

class Lattice {公共:使用Spin = int8_t;格子(int const size):size_(size),data_(size * size){} Spin&运算符()(int const x,int const y){返回data_ [y * size_ + x]; }旋转const&运算符()(int const x,int const y)const {返回data_ [y * size_ + x]; } int size()const {return size_; } private:int size_; std :: vector<旋转> data_; };

在很多解决方案中,我都看到了一些成员函数,例如double Lattice :: energy()const,double Lattice :: magnetization()const和void Lattice :: do_update()。他们不属于这一类!该类的不变量是:

函数能量应该被实现为自由的双能量(Lattice const& lattice),然后可以使用Lattice :: operator()const计算晶格的总能量。

当要从中派生一个类时,析构函数应该是虚拟的,否则可能存在内存泄漏。以这个非常简单的示例为例,我们有一个没有成员且没有虚函数的基类。现在,派生类(Base的作者无法控制该派生类)引入带有new和delete的手动资源管理。在用户代码中,我们使用动态多态性。当unique_ptr被破坏时,它将在其内部指针上调用delete。然后将调用Base ::〜Base,它是编译器提供的非虚拟空函数。

#include< iostream> #include< memory> class Base {public:void hello(){std :: cout<< "你好,世界" << std :: endl; } // virtual〜Base(){}}; class Derived:public Base {public:Derived():p(new int [1]){}〜Derived(){delete p; }私人的:int * p; }; int main(){std :: unique_ptr<基础> base(new Derived()); }

缺少虚拟构造函数意味着Base没有虚拟函数表,并且派生类也没有机会覆盖析构函数。在此示例中,这引入了内存泄漏,该泄漏可通过泄漏清理器(-fsanitize = leak)直接找到:

== 31756 ==错误:LeakSanitizer:检测到内存泄漏1个对象分配了4个字节的直接泄漏,分配自:#0 0 x7f520ce6d605在运算符new [](unsigned long)(/ lib64 / liblsan .so中)。 0 + 0 xe605)#1 0 x400975 in派生:: ::派生()(/ home / mu / Projekte / eigene-webseite /文章/ cpp-反模式/ a。out + 0 x400975)#2 0 x4008c6在main(/ home / mu / Projekte / eigene-webseite /文章/ cpp-反模式/ a .out + 0 x4008c6)#3 0 x7f520c1e0f29 in __libc_start_main(/ lib64 / libc .so .6 + 0 x20f29)摘要:LeakSanitizer:4字节(s)在1个分配中泄漏。

不应在普通代码中使用前导下划线来定义。 这些保留给标准库或编译器使用。 在像AVX这样的内部语言进行编程时,则必须使用以下划线开头的函数和类型。 但是不允许使用前导下划线定义新类型。 确实不需要__。 仅拥有MY_AWESOME_HEADER_H也可以。 也许只是#pragma一次,但这是一个不同的辩论。 几年来,出现了clang格式。 如果不使用它,则会严重丢失。 它将完美格式化整个代码库,并附带一个编辑器插件。 您不必担心缩进,那将是完美的。 不幸的是,很难将其应用于具有很多功能的代码库 ......