你最喜欢的C++编码风格成语是什么?

What are your favorite C++ Coding Style idioms

你最喜欢的C++编码风格成语是什么?我问的是样式或编码排版,比如在哪里放大括号、关键字后是否有空格、缩进大小等。这与最佳实践或要求相反,例如总是使用delete[]删除数组。

下面是我最喜欢的一个例子:在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中已经过时了。应使用范围枚举(用enum classenum struct声明):它更安全、简洁和灵活。使用旧的样式枚举,值被放置在外部作用域中。通过新的枚举方式,它们被放在enum class名称的范围内。上一个使用作用域枚举(也称为强类型枚举)重写的示例:

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*/
    }
}

使用范围枚举还有其他显著的好处:没有隐式强制转换、可能的正向声明以及使用自定义基础类型的能力(不是默认的int)。


拷贝交换

复制交换习语提供了异常安全的复制。它要求实现正确的复制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]如果抛出异常,则不修改对象的状态(*this)。


奇怪的重复模板模式

将类作为模板参数传递给其基类时,将发生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习惯用法是将类的接口与其实现分离的非常有用的方法。

通常,类定义必须包含成员变量和方法,这可能会暴露太多的信息。例如,成员变量可能是在头中定义的类型,我们不希望在任何地方都包含该类型。

这里的windows.h头是一个主要示例。我们可能希望在一个类中包装一个HANDLE或另一个win32类型,但是我们不能将HANDLE放在类定义中,而不必在使用该类的所有地方都包含windows.h

然后,解决方案是创建一个私有实现或类的实现指针,让公共实现只存储一个指向私有实现的指针,并转发所有成员方法。

例如:

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(); }

foo的实现现在与公共接口分离,因此

  • 它可以使用其他头中的成员和类型,而不需要在使用类时存在这些依赖项,以及
  • 可以在不强制重新编译使用类的代码的情况下修改实现。

类的用户只包括头,它不包含关于类实现的任何特定内容。所有实施细节都包含在foo.cpp中。


我喜欢在"列"中排列代码/初始化…证明了非常有用的编辑与"列"模式的编辑能力,也似乎是一个更容易我阅读…

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_;
};

if语句中,当存在困难条件时,可以清楚地显示每个条件使用缩进的级别。

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;
}

可以使用定义了二进制*运算符的任何正则类型调用twice。同样,可以使用任何可比较的类型和模型输入迭代器调用find()。一组代码在不同的类型上运行类似,没有看到共享的基类。

当然,这里真正要做的是,在模板实例化时,将相同的源代码扩展为各种类型特定的函数,每个函数都有单独生成的机器代码。在没有模板的情况下适应相同的类型集需要1)具有特定签名的独立手写函数,或2)通过虚拟函数实现运行时多态性。


雷迪克

我修复了将长语句拆分为太多短行的代码。

让我们面对现实吧:现在已经不是90年代了。如果你的公司负担不起宽屏液晶显示器的编码器,你需要找一份更好的工作:)


没有收藏夹,但我将修复包含以下内容的代码:

  • 标签-导致许多IDE和代码评审工具中的不一致,因为它们并不总是在mod 8空格处的标签上达成一致。
  • 超过80列的行-让我们面对它,较短的行更可读。我的大脑可以解析大多数编码约定,只要行很短。
  • 带有尾随空格的行-git会抱怨它是空格错误,在diff中显示为红色斑点,这很烦人。
  • 下面是一行代码,用于查找有问题的文件:

    1
    git grep -I -E '<tab>|.{81,}|  *$' | cut -f1 -d: | sort -u

    其中是制表符(posix regexp不起作用)


    模板和挂钩

    这是一种在框架中尽可能多地进行处理的方法,并为框架的用户定制提供一个门或钩子。也称为热点和模板方法。

    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 */
    }

    尽管这让我的同事很恼火。您可以一目了然地看出,我在编码过程中考虑了其他情况。