你最喜欢的 C + + 编码风格的习惯用法是什么

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

下面是我最喜欢的一个例子: 在 C + + Class 初始化器中,我们将分隔符放在行的前面,而不是后面。这样更容易保持最新。这也意味着不同版本之间的源代码控制更加清晰。

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 ] )
{
;
}
29122 次浏览

将每个方法或函数参数写在单独的行上,以便于注释。

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.    */
)

在函数行上记录返回值,因此很容易找到它们。

int function(void) /* return 1 on success, 0 on failure */
{
return 1;
};

不知道这算不算一个习惯用法,但我倾向于使用 氧气风格的内联注释,即使项目还没有使用 doxygen..。

bool MyObjects::isUpToSomething() ///< Is my object up to something

(除此之外,我的评论通常没有那么蹩脚。)

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

if (  (  (var1A == var2A)
|| (var1B == var2B))
&& (  (var1C == var2C)
|| (var1D == var2D)))
{
// do something
}

我喜欢在“列”中排列代码/初始化... 证明在用“列”模式编辑时非常有用,而且似乎对我来说更容易阅读..。

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

相比之下,没有缩进和格式化的代码将会出现... ... (对我的眼睛来说有点难读)

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. 制表符-在许多 IDE 和代码审查工具中导致不对齐,因为它们并不总是在 mod 8空格的制表符上达成一致。
  2. 长度超过80列的行-让我们面对现实,较短的行更易于阅读。我的大脑可以解析大多数编码约定,只要行很短。
  3. 带有尾随空格的行-git 会抱怨它是空格 错误,它会显示为红色的圆点,这很烦人。

下面是一个查找违规文件的小例子:

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

其中 <tab>是制表符(POSIX regexp 不做 t)

创建枚举时,将它们放在命名空间中,以便您可以使用有意义的名称访问它们:

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名称的范围内。
前面的示例使用作用域枚举(也称为 强类型枚举)重写:

enum class EntityType {
Ground = 0,
Human,
Aerial,
Total
};


void foo(EntityType entityType)
{
if (entityType == EntityType::Ground) {
/*code*/
}
}

使用作用域枚举还有其他显著的好处: 不存在隐式转换、可能存在前向声明、以及能够使用自定义基础类型(而不是默认的 int)。

回答: ididak

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

面对现实吧,现在已经不是90年代了。 如果你的公司买不起宽屏液晶显示器给它的程序员,你需要找一个更好的工作:)

我真的很喜欢把一个小语句放在 if 的同一行上

int myFunc(int x) {
if(x >20) return -1;
//do other stuff ....
}

我总是吹毛求疵,编辑以下内容:

  • 多余的新台词
  • EOF 没有换行

将函数名放在一个新行上是很有用的,这样您就可以像

grep -R '^fun_name' .

我见过很多 GNU 项目使用这种风格,并且很喜欢:

static void
fun_name (int a, int b) {
/* ... */
}

我通常坚持使用 * BSD 风格(9)中描述的 KNF

在和一个半盲人一起工作之后——在他的要求下——我转而使用更多的空间。我当时不喜欢它,但现在我更喜欢它。在我的脑海中,唯一一个标识符和关键字之间没有空格的地方是 之后函数名,在下面的括号之前。

void foo( int a, int b )
{
int c = a + ( a * ( a * b ) );
if ( c > 12 )
c += 9;
return foo( 2, c );
}

我倾向于在我所有的假设上加上别的。

if (condition)
{
complicated code goes here
}
else
{
/* This is a comment as to why the else path isn't significant */
}

尽管这让我的同事很不爽。 您可以一眼看出,我在编码期间考虑了 else 情况。

RAII

RAII 可能是最重要的习惯用法。它的思想是,应该将资源映射到对象,以便根据声明这些对象的范围自动管理它们的生存期。

例如,如果在堆栈上声明了一个文件句柄,那么当我们从函数(或者循环,或者在其中声明的任何作用域)返回时,它应该被隐式关闭。如果动态内存分配是作为类的成员分配的,那么当该类实例被销毁时,应该隐式释放该动态内存分配。诸如此类。每一种资源、内存分配、文件句柄、数据库连接、套接字以及任何其他需要获取和释放的资源都应该包装在这样一个 RAII 类中,其生存期由声明它的作用域决定。

这样做的一个主要优点是,C + + 保证在对象超出范围 无论控制如何离开该范围时调用析构函数。即使抛出异常,所有本地对象都将超出范围,因此它们相关的资源将被清除。

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 允许代码“在抛出异常时自动调用‘撤消’操作。”

PImpl: 指向实现的指针

PImpl 习惯用法是将类的接口与其实现分离的一种非常有用的方法。

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

windows.h头是这里的一个主要例子。我们可能希望在类中包装一个 HANDLE或另一个 Win32类型,但是我们不能在类定义中放置一个 HANDLE,而不必在使用类的任何地方包含 windows.h

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

例如:

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中。

我建议使用 PIMPL,或者詹姆斯 · 科普林最初称之为“控制身体”。

这个习惯用法允许您将接口与实现完全解耦。当处理一个主要 CORBA 中间件组件的重写和重新发布时,这个习惯用法被用来将 API 与实现完全解耦。

这几乎排除了任何可能的逆向工程。

James Coplien 的优秀著作“ 高级 C + + 编程风格与习语”是 C + + 习惯用法的一个优秀资源,强烈推荐!

编辑: 正如 Neil 在下面指出的那样,这本书已经过时了,他的许多建议实际上已经纳入了 C + + 标准本身。然而,我仍然发现它是一个有用的信息来源,特别是。在他的 关于 C + + 习语的 PLoP 论文的形式,其中许多习语被重新铸造成模式形式。

奇异递归模板模式

当您将一个类作为模板参数传递给它的基类时,会发生 CRTP :

template<class Derived>
struct BaseCRTP {};


struct Example : BaseCRTP<Example> {};

在基类中,它可以通过强制转换(静态播放Dynamic _ cast工作)获得派生实例 与派生类型一起完成:

template<class Derived>
struct BaseCRTP {
void call_foo() {
Derived& self = *static_cast<Derived*>(this);
self.foo();
}
};


struct Example : BaseCRTP<Example> {
void foo() { cout << "foo()\n"; }
};

实际上,呼叫 _ foo已经被 注射加入到派生类中,可以完全访问派生类的成员。

随意编辑和添加特定的使用示例,可能到 其他特别职位

拷贝交换

复制交换习惯用法提供了异常安全的复制。它要求实现正确的复制 ctor 和交换。

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)的状态。

确切地说,我不知道它是否符合习惯用法,但是相当多的重型模板编程(通常很大程度上)依赖于 SFINAE (替换失败不是错误)。先前的一个问题的几个答案有一些例子。

编译时多态性

(也称为语法多态性和静态多态性,与运行时多态性相对应。)

使用模板函数,可以编写依赖于类型构造函数的代码,并调用参数化类型家族的签名,而无需引入公共基类。

程序设计元素一书中,作者将这种类型的处理称为 抽象属。使用 概念可以指定这些类型参数的要求,尽管 C + + 并不强制要求这样的规范。

两个简单的例子:

#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)通过虚函数实现运行时多态性。

模板和钩子

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

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
}

沃尔夫冈 · 普雷在他的书 面向对象软件开发的设计模式中描述。

使用空格分隔符括起来的表达式

if (expression)  // preferred - if keyword sticks out more

对。

if(expression)  // looks too much like a void function call

我想这意味着我喜欢函数调用没有空格分隔符

foo(parm1, parm2);

公共上层-私人下层

这似乎是一个小小的优化,但自从我转向这个惯例,我有了更多的方法来掌握我的课程,尤其是在我42年没有看过它们之后。

拥有一致的成员可见性,从经常感兴趣的点到无聊的东西,是非常有帮助的,特别是当代码应该是自我文档化的时候。

(qt 用户的旁注: 插槽在信号之前,因为它们应该像非插槽成员函数一样可调用,而且除了它们的插槽性之外,它们与非插槽无法区分)

  • 公共的,受保护的,私人的
  • 然后是工厂,ctor,dtor,复制,交换
  • 然后是类的接口 最后,在一个单独的 private:部分中,将显示数据(理想情况下仅显示一个 impl 指针)。

如果您在保持类声明的整洁方面存在问题,那么这条规则也非常有用。

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