What are your favorite C++ Coding Style idioms
你最喜欢的C++编码风格成语是什么?我问的是样式或编码排版,比如在哪里放大括号、关键字后是否有空格、缩进大小等。这与最佳实践或要求相反,例如总是使用
下面是我最喜欢的一个例子:在C++类初始化器中,我们把分隔符放在行的前面,而不是后面。这使得更新更容易。它还意味着版本之间的源代码控制差异更为明显。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | TextFileProcessor:: TextFileProcessor( class ConstStringFinder& theConstStringFinder ) : TextFileProcessor_Base( theConstStringFinder ) , m_ThreadHandle ( NULL ) , m_startNLSearch ( 0 ) , m_endNLSearch ( 0 ) , m_LineEndGetIdx ( 0 ) , m_LineEndPutIdx ( 0 ) , m_LineEnds ( new const void*[ sc_LineEndSize ] ) { ; } |
RAII:资源获取正在初始化
RAII可能是最重要的成语。资源应该映射到对象上,以便根据声明这些对象的范围自动管理它们的生命周期。
例如,如果在堆栈上声明了一个文件句柄,那么当我们从函数(或循环,或在其中声明的任何范围)返回时,它应该隐式关闭。如果动态内存分配是作为类的成员分配的,那么在销毁该类实例时,应该隐式释放它。等等。每种资源(内存分配、文件句柄、数据库连接、套接字以及任何其他必须获取和释放的资源)都应该包装在这样一个raii类中,该类的生存期由声明它的范围决定。
这其中的一个主要优点是,C++保证在对象超出范围时调用析构函数,而不管控件是如何离开该范围的。即使抛出异常,所有本地对象也将超出范围,因此它们的关联资源将被清除。
1 2 3 4 5 6 7 8 9 10 | void foo() { std::fstream file("bar.txt"); // open a file"bar.txt" if (rand() % 2) { // if this exception is thrown, we leave the function, and so // file's destructor is called, which closes the file handle. throw std::exception(); } // if the exception is not called, we leave the function normally, and so // again, file's destructor is called, which closes the file handle. } |
不管我们如何离开函数,不管打开文件后会发生什么,我们都不需要显式地关闭文件,也不需要处理函数中的异常(例如,Try Finally)。相反,文件会被清除,因为它绑定到一个本地对象,当它超出范围时,该对象会被销毁。
RAII也不太常见,称为SBRM(范围限制资源管理)。
参见:
- Scopeguard允许代码"自动调用"撤消操作。如果引发异常。"
创建枚举时,请将它们放在命名空间中,以便您可以使用有意义的名称访问它们:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | namespace EntityType { enum Enum { Ground = 0, Human, Aerial, Total }; } void foo(EntityType::Enum entityType) { if (entityType == EntityType::Ground) { /*code*/ } } |
编辑:但是,这种技术在C++ 11中已经过时了。应使用范围枚举(用
1 2 3 4 5 6 7 8 9 10 11 12 13 | enum class EntityType { Ground = 0, Human, Aerial, Total }; void foo(EntityType entityType) { if (entityType == EntityType::Ground) { /*code*/ } } |
使用范围枚举还有其他显著的好处:没有隐式强制转换、可能的正向声明以及使用自定义基础类型的能力(不是默认的
拷贝交换
复制交换习语提供了异常安全的复制。它要求实现正确的复制ctor和交换。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | struct String { String(String const& other); String& operator=(String copy) { // passed by value copy.swap(*this); // nothrow swap return *this; // old resources now in copy, released in its dtor } void swap(String& other) throw() { using std::swap; // enable ADL, defaulting to std::swap swap(data_members, other.data_members); } private: Various data_members; }; void swap(String& a, String& b) { // provide non-member for ADL a.swap(b); } |
还可以直接使用ADL(依赖于参数的查找)实现交换方法。
这个成语很重要,因为它处理自分配[1],使强异常保证[2],并且通常很容易编写。
[1]尽管自我分配没有尽可能有效地处理,但它应该是罕见的,所以如果它从未发生过,这实际上是更快的。
[2]如果抛出异常,则不修改对象的状态(
奇怪的重复模板模式
将类作为模板参数传递给其基类时,将发生CRTP:
1 2 3 4 | template<class Derived> struct BaseCRTP {}; struct Example : BaseCRTP<Example> {}; |
在基类中,它可以通过强制转换(静态强制转换或动态强制转换工作)来获取派生实例的旧版本,以及派生类型:
1 2 3 4 5 6 7 8 9 10 11 12 | template<class Derived> struct BaseCRTP { void call_foo() { Derived& self = *static_cast<Derived*>(this); self.foo(); } }; struct Example : BaseCRTP<Example> { void foo() { cout <<"foo() "; } }; |
实际上,调用foo已经注入到派生类中,可以完全访问派生类的成员。
请随意编辑和添加特定的使用示例,可能会添加到其他SO文章中。
pimpl:指向实现的指针
PIMPL习惯用法是将类的接口与其实现分离的非常有用的方法。
通常,类定义必须包含成员变量和方法,这可能会暴露太多的信息。例如,成员变量可能是在头中定义的类型,我们不希望在任何地方都包含该类型。
这里的
然后,解决方案是创建一个私有实现或类的实现指针,让公共实现只存储一个指向私有实现的指针,并转发所有成员方法。
例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | class private_foo; // a forward declaration a pointer may be used // foo.h class foo { public: foo(); ~foo(); void bar(); private: private_foo* pImpl; }; // foo.cpp #include whichever header defines the types T and U // define the private implementation class class private_foo { public: void bar() { /*...*/ } private: T member1; U member2; }; // fill in the public interface function definitions: foo::foo() : pImpl(new private_foo()) {} foo::~foo() { delete pImpl; } void foo::bar() { pImpl->bar(); } |
- 它可以使用其他头中的成员和类型,而不需要在使用类时存在这些依赖项,以及
- 可以在不强制重新编译使用类的代码的情况下修改实现。
类的用户只包括头,它不包含关于类实现的任何特定内容。所有实施细节都包含在
我喜欢在"列"中排列代码/初始化…证明了非常有用的编辑与"列"模式的编辑能力,也似乎是一个更容易我阅读…
1 2 3 4 5 6 7 8 9 10 11 | int myVar = 1; // comment 1 int myLongerVar = 200; // comment 2 MyStruct arrayOfMyStruct[] = { // Name, timeout, valid {"A string", 1000, true }, // Comment 1 {"Another string", 2000, false }, // Comment 2 {"Yet another string", 11111000, false }, // Comment 3 {NULL, 5, true }, // Comment 4 }; |
相反,相同的代码没有缩进和格式化如上所示…(对我来说读起来有点难)
1 2 3 4 5 6 7 8 9 10 11 | int myVar = 1; // comment 1 int myLongerVar = 200; // comment 2 MyStruct arrayOfMyStruct[] = { // Name, timeout, valid {"A string", 1000, true},// Comment 1 {"Another string", 2000, false }, // Comment 2 {"Yet another string", 11111000,false}, // Comment 3 {NULL, 5, true }, // Comment 4 }; |
公共顶部-私人向下
一个看似很小的优化,但自从我转向这个惯例,我有一个更有趣的时间来掌握我的课程,特别是在我已经42年没有看它们之后。
保持一致的成员可见性,从经常感兴趣的点到无聊的东西,是非常有用的,特别是当代码应该是自文档化的时候。
(Qt用户的旁注:插槽在信号之前,因为它们应该像非插槽成员函数一样可调用,并且除了其插槽之外,与非插槽不可区分)
- 公共、保护、私人
- 然后是工厂、ctor、dtor、复制、交换
- 然后是类的接口最后,在一个单独的
private: 部分中,会出现数据(理想情况下,只有一个简单指针)。
如果您在保持类声明的整洁性方面遇到问题,此规则也会有很大帮助。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | class Widget : public Purple { public: // Factory methods. Widget FromRadians (float); Widget FromDegrees (float); // Ctors, rule of three, swap Widget(); Widget (Widget const&); Widget &operator = (Widget const &); void swap (Widget &) throw(); // Member methods. float area() const; // in case of qt {{ public slots: void invalidateBlackHole(); signals: void areaChanged (float); // }} protected: // same as public, but for protected members private: // same as public, but for private members private: // data float widgetness_; bool isMale_; }; |
在
1 2 3 4 5 6 7 | if ( ( (var1A == var2A) || (var1B == var2B)) && ( (var1C == var2C) || (var1D == var2D))) { // do something } |
编译时多态性
(也被称为句法多态性和静态多态性,与运行时多态性形成对比。)
使用模板函数,可以编写依赖类型构造函数的代码,并调用参数化类型族的签名,而无需引入公共的基类。
在《编程元素》一书中,作者将这种类型的处理称为抽象属。通过概念,人们可以指定对此类类型参数的要求,尽管C++没有授权这样的规范。
两个简单示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | #include <stdexcept> template <typename T> T twice(T n) { return 2 * n; } InIt find(InIt f, InIt l, typename std::iterator_traits<InIt>::reference v) { while (f != l && *f != v) ++f; return f; } int main(int argc, char* argv[]) { if (6 != twice(3)) throw std::logic_error("3 x 2 = 6"); int const nums[] = { 1, 2, 3 }; if (nums + 4 != find(nums, nums + 4, 42)) throw std::logic_error("42 should not have been found."); return 0; } |
可以使用定义了二进制
当然,这里真正要做的是,在模板实例化时,将相同的源代码扩展为各种类型特定的函数,每个函数都有单独生成的机器代码。在没有模板的情况下适应相同的类型集需要1)具有特定签名的独立手写函数,或2)通过虚拟函数实现运行时多态性。
雷迪克
我修复了将长语句拆分为太多短行的代码。
让我们面对现实吧:现在已经不是90年代了。如果你的公司负担不起宽屏液晶显示器的编码器,你需要找一份更好的工作:)
没有收藏夹,但我将修复包含以下内容的代码:
下面是一行代码,用于查找有问题的文件:
1 | git grep -I -E '<tab>|.{81,}| *$' | cut -f1 -d: | sort -u |
其中
模板和挂钩
这是一种在框架中尽可能多地进行处理的方法,并为框架的用户定制提供一个门或钩子。也称为热点和模板方法。
1 2 3 4 5 6 7 8 | class Class { void PrintInvoice(); // Called Template (boilerplate) which uses CalcRate() virtual void CalcRate() = 0; // Called Hook } class SubClass : public Class { virtual void CalcRate(); // Customized method } |
WolfgangPree在他的书中描述了面向对象软件开发的设计模式。
确切地说,我不知道它是否适合作为一个习惯用法,但是相当多的重载模板编程依赖于(通常很大程度上)sfinae(替换失败不是一个错误)。前一个问题的几个答案都有例子。
if/while/用于带空格分隔符的括号表达式
1 | if (expression) // preferred - if keyword sticks out more |
VS
1 | if(expression) // looks too much like a void function call |
我想这意味着我喜欢我的函数调用没有空格分隔符
1 | foo(parm1, parm2); |
在和一个部分失明的人一起工作之后——在他的要求下——我转而使用更多的空间。当时我不喜欢,但现在我更喜欢。在我的头顶上,唯一一个标识符和关键字之间没有空格的地方,什么都没有,在函数名之后,在下面的圆括号之前。
1 2 3 4 5 6 7 | void foo( int a, int b ) { int c = a + ( a * ( a * b ) ); if ( c > 12 ) c += 9; return foo( 2, c ); } |
我真的喜欢把一个小小的声明放在同一行
1 2 3 4 | int myFunc(int x) { if(x >20) return -1; //do other stuff .... } |
不确定这是否算是一个习惯用法,但我倾向于使用doxygen风格的内联注释,即使在项目还没有使用doxygen的时候……
1 | bool MyObjects::isUpToSomething() ///< Is my object up to something |
(旁白)我的评论通常不是那么蹩脚。)
将函数名放在新行上很有用,这样您就可以
1 | grep -R '^fun_name' . |
对他们来说。我见过用于大量GNU项目的样式,并且喜欢它:
1 2 3 4 | static void fun_name (int a, int b) { /* ... */ } |
将每个方法或函数参数写在单独的行上,这样就可以很容易地对其进行注释。
1 2 3 4 5 | int ReturnMaxValue( int* inputList, /* the list of integer values from which to get the maximum */ long size, /* count of the number of integer values in inputList */ char* extraArgs /* additional arguments that a caller can provide. */ ) |
我建议皮姆普或者詹姆斯·科普林最初称之为"把手身体"。
这个习语允许您将接口与实现完全分离。在重写和重新发布一个主要的CORBA中间件组件时,使用这个习语将API与实现完全分离。
这实际上消除了任何逆向工程的可能性。
C++习语的一个很好的资源是James Coplien的优秀书籍《高级C++编程风格和成语》。强烈推荐!
编辑:正如尼尔所指出的,这本书已经过时了,他的许多建议实际上都被纳入了C++标准本身。然而,我仍然认为它是有用信息的来源,尤其是他在C++习语中的形式,其中许多成语被改写成词组形式。
在函数行中记录返回值,这样很容易找到它们。
1 2 3 4 | int function(void) /* return 1 on success, 0 on failure */ { return 1; }; |
我总是吹毛求疵并编辑以下内容:
- 多余的换行符
- EOF没有换行
我通常坚持使用*BSD样式(9)中描述的KNF。
我倾向于在所有的假设上加一个别的。
1 2 3 4 5 6 7 8 | if (condition) { complicated code goes here } else { /* This is a comment as to why the else path isn't significant */ } |
尽管这让我的同事很恼火。您可以一目了然地看出,我在编码过程中考虑了其他情况。