编码实践总结
工具
- 代码格式化clang_format
---
# .clang-format
# This configuration requires clang-format version 6.0 exactly.
BasedOnStyle: WebKit
---
Language: Cpp
ColumnLimit: 90
...
- C++代码风格Google Style
- 静态分析工具Cppcheck
- Visual Studio扩展Visual Assist
- 跨平台的编译工具CMake
模板
模板的声明和实现最好都在头文件中,隐式实例化。
除非,模板特化,显式实例化。
特化
函数模板特化:
#include <iostream>
using namespace std;
template <typename T> T Max(T t1, T t2) { return (t1 > t2) ? t1 : t2; }
typedef const char* CCP;
template <> CCP Max<CCP>(CCP s1, CCP s2) { return (strcmp(s1, s2) > 0) ? s1 : s2; }
int main()
{
//调用实例:int Max<int>(int,int)
int i = Max(10, 5);
//调用显示特化:const char* Max<const char*>(const char*,const char*)
const char* p = Max<const char*>("very", "good");
cout << "i:" << i << endl;
cout << "p:" << p << endl;
}
类模板特化:
#include <iostream>
using namespace std;
template<typename T>class A{
T num;
public:
A(){
num = T(6.6);
}
void print(){
cout << "A'num:" << num << endl;
}
};
template<>class A<char*>{
char* str;
public:
A(){
str = "A' special definition ";
}
void print(){
cout << str << endl;
}
};
int main(){
A<int> a1; //显示模板实参的隐式实例化
a1.print();
A<char*> a2; //使用特化的类模板
A2.print();
}
显示实例化
隐藏细节,减少编译器耗时。
// stack.h
#ifndef STACK_H_
#define STACK_H_
#include <vector>
#include <string>
template <typename T> class Stack {
public:
void Push(T val);
T Pop();
bool IsEmpty() const;
private:
std::vector<T> stack_;
};
typedef Stack<int> IntStack;
typedef Stack<double> DoubleStack;
typedef Stack<std::string> StringStack;
#endif // STACK_H_
// stack.cpp
#include "stack.h"
template <typename T> void Stack<T>::Push(T val) { stack_.push_back(val); }
template <typename T> T Stack<T>::Pop()
{
if (IsEmpty()) {
return T();
}
T val = stack_.back();
stack_.pop_back();
return val;
}
template <typename T> bool Stack<T>::IsEmpty() const { return stack_.empty(); }
// 显式类模板实例化
template class Stack<int>;
template class Stack<double>;
template class Stack<std::string>;
宏
- 条件编译调试代码
#ifdef USER_DEBUG
std::ofstream ofs("out.raw", std::ios::binary);
if(ofs.is_open())
{
ofs.write(data, size);
ofs.close();
}
#endif // USER_DEBUG
- 宏定义
#
,##
用法
#include<cstdio>
#define STR(s) #s
#define CONS(a,b) int(a##e##b)
int main()
{
printf(STR(vck)); // 输出字符串"vck"
printf("%d/n", CONS(2,3)); // 2e3 输出:2000
return 0;
}
原则
接口的改动最好不要影响之前的调用
例1
原接口:
int main(int argc, char* argv[])
{
if (argc == 4) {
bOnline = (std::string(argv[3]) == std::string("Online"));
sRawSaveFullPath = std::string(argv[2]);
sParaXmlPath = std::string(argv[1]);
} else if (argc == 3) {
bOnline = false;
sRawSaveFullPath = std::string(argv[2]);
sParaXmlPath = std::string(argv[1]);
} else if (argc == 2) {
bOnline = false;
sRawSaveFullPath = std::string("d:\\temp\\ProcessedRaw.raw");
sParaXmlPath = std::string(argv[1]);
} else {
LOG2_ERROR(m_pLogger,
"Parameter wrong! \n"
<< "[Usage] Simulator.exe ParaXmlFullPath [RawSaveFullPath] "
"[Online]");
return -1;
}
}
修改 (bad):
int main(int argv, char* argv[])
{
if (argc == 5) {
strxmlversion = std::string(argv[4]);
if (strxmlversion == "1")
xmlversion = 1;
else if (strxmlversion == "0")
xmlversion = 0;
bOnline = (std::string(argv[3]) == std::string("Online"));
sRawSaveFullPath = std::string(argv[2]);
sParaXmlPath = std::string(argv[1]);
} else if (argc == 4) {
bOnline = false;
strxmlversion = std::string(argv[3]);
if (strxmlversion == "1")
xmlversion = 1;
else if (strxmlversion == "0")
xmlversion = 0;
sRawSaveFullPath = std::string(argv[2]);
sParaXmlPath = std::string(argv[1]);
} else if (argc == 3) {
strxmlversion = std::string(argv[2]);
if (strxmlversion == "1")
xmlversion = 1;
else if (strxmlversion == "0")
xmlversion = 0;
bOnline = false;
sRawSaveFullPath = std::string("d:\\temp\\ProcessedRaw.raw");
sParaXmlPath = std::string(argv[1]);
} else if (argc == 2) {
xmlversion = 0;
bOnline = false;
sRawSaveFullPath = std::string("d:\\temp\\ProcessedRaw.raw");
sParaXmlPath = std::string(argv[1]);
} else {
LOG2_ERROR(m_pLogger,
"Parameter wrong! \n"
<< "[Usage] Simulator.exe ParaXmlFullPath [RawSaveFullPath] "
"[Online]");
return -1;
}
}
原代码中我们的调用方式:
Simulator.exe D:\SSIT\SSITData\Temp.xml D:\SSIT\SSITOutPut\Temp.raw
修改后,我们不得不修改调用方式为:
Simulator.exe D:\SSIT\SSITData\Temp.xml D:\SSIT\SSITOutPut\Temp.raw 0
或
Simulator.exe D:\SSIT\SSITData\Temp.xml D:\SSIT\SSITOutPut\Temp.raw 1
例2:
原接口:
int Fun(
int iSelectedMethod,
double* dOffSet2d[2],
double* dOffset3d,
double dProgressStart = 0,
double dProgressEnd = 1,
IProgress* pProgress = 0);
修改1 (bad):
int Fun(
int iSelectedMethod,
double* dOffSet2d[2],
double* dOffset3d,
unsigned char* pDRRmask[2] = nullptr, //
Mcsf::PARAMETER_t* para = nullptr, //
double dProgressStart = 0,
double dProgressEnd = 1,
IProgress* pProgress = 0);
修改2 (good):
int Fun(
int iSelectedMethod,
double* dOffSet2d[2],
double* dOffset3d,
unsigned char* pDRRmask[2] = nullptr, //
Mcsf::PARAMETER_t* para = nullptr, //
double dProgressStart = 0,
double dProgressEnd = 1,
IProgress* pProgress = 0,
bool bIfUseFireflyMethod = true, // false
bool bIfFastMode = true,
short* pRegistered0Degree = nullptr,
short* pRegistered90Degree = nullptr,
int iSimilarityMeasureTypeforFirefly = 1,
int iMutualInformationType = 1,
bool bifGradientAdjust = true); // false
变量定义后一定要初始化
struct CaliParam {
public:
GainCaliParam()
{
fDose = 0.1f;
iRowSize = 0;
iColumnSize = 0;
iBins = 0;
iMarginLeft = 0;
iMarginRight = 0;
iMarginTop = 0;
iMarginBottom = 0;
}
~CaliParam() { }
public:
float fDose;
float fPulse;
int iRowSize;
int iColumnSize;
int iBins;
int iMarginLeft;
int iMarginRight;
int iMarginTop;
int iMarginBottom;
};
// 调用
bool signal = true;
int viewNum = 0;
cali_para_->fDose = 0;
while (signal) {
auto token = pPipeReader->Read(1);
if (1 > token->Size()) {
signal = false;
break;
}
cali_para_->fDose += token->Get(0).FrameDose;
cali_para_->fPulse += token->Get(0).FramePulse;
// code ...
viewNum++;
}
#ifdef _DEBUG
不要在同一项目的不同工程中出现同名
包括类名,函数名,全局变量,等。
// A工程,算法的 CPU 实现
class AlgorithmClass {
}
// B工程,算法的 GPU 实现
class AlgorithmClass {
}
// C工程同时链接 A 工程和 B 工程,分情况调用 GPU 和 CPU 版的算法,
// 调用哪个类可能是未定义的
class AlgorithmClassProxy {
}
// 正确的做法
// A工程,一个算法的 CPU 实现
namespace Img {
namespace Cpu {
class AlgorithmClass {
}
} // namespace Img
} // Cpu
// B工程,一个算法的 GPU 实现
namespace Img {
namespace Gpu {
class AlgorithmClass {
}
} // namespace Img
} // Cpu
// C工程同时链接 A 工程和 B 工程
class AlgorithmClassProxy {
}
自定义类型要明确复制和赋值构造函数
struct MyStruct{
float width_in_mm;
int width_in_pixel;
std::vector<unsigned short> pixel_data; // 深拷贝
}
struct MyStruct{
float width_in_mm;
int width_in_pixel;
unsigned short *pixel_data; // 浅拷贝
}
对于以上两种定义,我们都应该明确复制和赋值构造函数,以避免一些未知的错误。
template<typename T>
struct MyStruct{
float width_in_mm;
int width_in_pixel;
T pixel_data; // 浅拷贝
private:
// 禁止无意义的拷贝和赋值构造函数
MyStruct(const MyStruct &);
MyStruct &operator=(const MyStrcut &);
}
设计模式
Pimpl惯用法
隐藏内部细节。
// autotimer.h
#if defined(_WIN32) || defined(_WIN64)
# include <windows.h>
#else
#include <sys/time.h>
#endif
IMG_BEGIN_NAMESPACE
class AutoTimer
{
public:
AutoTimer();
~AutoTimer();
private:
double GetElapsed() const;
#if defined(_WIN32) || defined(_WIN64)
DWORD mStartTime;
#else
struct timeval mStartTime;
#endif
}
IMG_END_NAMESPACE
实现这个类的目的只是为了获取对象从构造到销毁之间的运行时间,与程序在是什么平台上跑完全没有关系。在这个头文件中暴露了太多的细节,破坏了接口的简洁性。
// autotimer.h
#include <memory>
IMG_BEGIN_NAMESPACE
class AutoTimer
{
public:
AutoTimer();
~AutoTimer();
private:
class Impl; //! 声明为私有,在CPP文件里的其他类或者自由函数不能访问Impl
Impl *mImpl;
}
IMG_END_NAMESPACE
#endif // IMG_SYSTEM_STATE_HPP_H_
// autotimer.cpp
#include <iostream>
#if defined(_WIN32) || defined(_WIN64)
# include <windows.h>
#else
#include <sys/time.h>
#endif
IMG_BEGIN_NAMESPACE
class AutoTimer::Impl
{
public:
double GetElapsed() const;
#if defined(_WIN32) || defined(_WIN64)
DWORD mStartTime;
#else
struct timeval mStartTime;
#endif
}
AutoTimer::AutoTimer() : mImpl(new AutoTimer::Impl())
{
#if defined(_WIN32) || defined(_WIN64)
mImpl->mStartTime = GetTickCount();
#else
gettimeofday(&(mImpl->mStartTime), NULL);
#endif
}
AutoTimer::~AutoTimer()
{
std::cout << "Elapsed time: " << mImpl->GetElapsed() << std::endl;
}
IMG_END_NAMESPACE
那么,什么样的函数或者数据成员应该放到Implementation类中呢?
- 仅私有成员变量
- 私有成员变量和方法
- 公有类的所有方法,其中公有方法只是对
Impl
类中的等价方法进行简单的包装
多倾向与采用在Impl
类中放置私有成员变量和方法的逻辑。
Note:
使用Pimp
惯用法时,应采用私有内嵌实现类,以更好地隐藏细节。只有在.cpp
文件中其他类或者自由函数必须访问Impl
成员时,才应采用公有内嵌类。
注意:复制语义,常使用智能指针建立Impl
类的实例。
单例模式
只创建一个对象实例。
- 私有化默认构造函数
- 私有化赋值和赋值构造函数
- 私有析构函数
- 返回指针或者引用
class Singleton
{
public:
static Singleton &GetInstance();
private:
Singleton();
~Singleton();
Singleton(const Singleton &);
const Singleton &operator=(const Singleton &);
}
不同编译单元中的非局部静态对象的初始化顺序是未定义的。
非局部对象是指在函数之外声明的对象,为了保证初始化顺序,在类的方法中创建静态变量。
Singleton &Singleton::GetInstance()
{
static Singleton instance;
return instance;
}
注意:上述实现不是线程安全的! 通常的做法是加互斥锁。
static Mutex mutex;
Singleton &Singleton::GetInstance()
{
ScopedLock lock(&mutex);
static Singleton instance;
return instance;
}
缺点:由于每次调用GetInstance()
函数时都会尝试获取锁,介绍调用时都会释放锁,这样的方法开销大。
static Mutex mutex;
Singleton &Singleton::GetInstance()
{
static Singleton *instance = nullptr;
if (nullptr == instance)
{
ScopedLock lock(&mutex);
if(nullptr == instance)
{
instance = new Singleton();
}
}
return *instance;
}
静态初始化:
Singleton &Singleton::GetInstance()
{
static Singleton instance;
return instance;
}
static Singleton &ins = Singleton::GetInstance();
显式API初始化,在程序已启动便首先调用APIInitialize
函数:
static std::mutex mut;
void APIInitialize()
{
std::lock_guard<std::mutex> lock(mut);
Singleton::GetInstance();
}
扩展,单一状态模式
class Monostate
{
public:
int GetTheAnswer() const { return m_s_answer;}
private:
static int m_s_answer;
}
int Monstate::m_s_answer = 16;
工厂模式
隐藏派生类实现细节。
// renderfactory.h
#include "render.h"
#include <string>
class RenderFactory
{
public:
IRender *CreateRender(const std::string &type);
}
// renderfactory.cpp
// 假设已经存在`OpenGLRender`, `DirectXRender`, 和 `MesaRender`三个派生类
#include "renderfactory.h"
#include "openglreder.h"
#include "directxrender.h"
#include "mesarender.h"
IRender *RenderFactory::CreateRender(const std::string &type)
{
if(type == "opengl")
{
return new OpenGLRender();
}
if(type == "directx")
{
return new DirectXRender();
}
if(type == "mesa")
{
return new MesaRender();
}
return NULL;
}
该类的缺陷是:包含了可用的派生类的硬编码信息。
扩展工厂示例:
// renderfacory.h
#include "render.h"
#include <string>
#include <map>
class RenderFactory
{
public:
typedef IRender *(*CreateCallback)();
static void RegisterRender(const std::string &type, CreateCallback cb);
static void UnRegisterReder(const std::string &type);
static IRender *CreateRender(const std::string &type);
private:
typedef std::map<std::string, CreateCallback> CallbackMap
static CallbackMap m_renders;
}
// renderfactory.cpp
#include "randerfactory.h"
RenderFactory::CallBackMap RenderFactory::m_renders;
void RenderFactory::RegisterRender(const std::string &type, CreateCallback cb)
{
m_render[type] = cb;
}
void RenderFactory::UnRegisterReder(const std::string &type)
{
if(m_render.find(type) != m_render.end())
{
m_render.erase(type);
}
}
IRender *RenderFactory::CreateRender(const std::string &type)
{
CallbackMap::iterator it = m_render.find(type);
if(it != m_render.end())
{
return (it->second)();
}
return NULL;
}
class UserRender: public IRender
{
public:
~UserRender(){}
void Render() { std::cout << "User render\n";}
static IRender *Create()
{
return new UserRender();
}
}
int main(void)
{
RenderFactory::ReginsterRender("user", UserRender::Create);
IRender *r = RenderFactory::CreateRender("user");
r->Render();
delete r;
return 0;
}
书籍
- 《Accelerated C++》
- 《Effective C++》
- 《C++ API 设计》
- 《大规模 C++ 设计》
- 《GoF23 中设计模式 C++》
- 《测试驱动开发》
- 《C++ 并行设计实践》
- 《C++ 沉思录》
多线程
互斥锁的使用
尽量用非递归互斥量,避免使用递归互斥量
Mutex分为递归(recursive)和非递归(non-recursive)两种,这是POSIX的叫法,另外的名字是可重入(reentrant
)与非可重入。这两种mutex作为线程间(inter-thread)的同步工具时没有区别,它们的惟一区别在于:同一个线程可以重复对recursive mutex加锁,但是不能重复对non-recursive mutex加锁。
首选非递归mutex,绝对不是为了性能,而是为了体现设计意图。non-recursive和recursive的性能差别其实不大,因为少用一个计数器,前者略快一点点而已。在同一个线程里多次对non-recursive mutex加锁会立刻导致死锁,我认为这是它的优点,能帮助我们思考代码对锁的期求,并且及早(在编码阶段)发现问题。
毫无疑问recursive mutex使用起来要方便一些,因为不用考虑一个线程会自己把自己给锁死了,我猜这也是Java和Windows默认提供recursive mutex的原因。(Java语言自带的intrinsic lock是可重入的,它的concurrent库里提供ReentrantLock,Windows的CRITICAL_SECTION也是可重入的。似乎它们都不提供轻量级的non-recursive mutex。)
正因为它方便,recursive mutex可能会隐藏代码里的一些问题。典型情况是你以为拿到一个锁就能修改对象了,没想到外层代码已经拿到了锁,正在修改(或读取)同一个对象呢。具体的例子:
std::vector<Foo> foos;
MutexLock mutex;
void post(constFoo &f)
{
MutexLock Guardlock(mutex);
foos.push_back(f);
}
void traverse()
{
MutexLock Guardlock(mutex);
for(auto it = foos.begin(); it != foos.end(); ++it)
{
it->doit();
}
}
post()
加锁,然后修改foos
对象;traverse()
加锁,然后遍历foos
数组。将来有一天,Foo::doit()
间接调用了post()
(这在逻辑上是错误的),那么会很有戏剧性的:
1.Mutex是非递归的话,于是死锁了。
2.Mutex是递归的的话,由于push_back可能(但不总是)导致vector迭代器失效,程序偶尔会crash。
这时候就能体现non-recursive的优越性:把程序的逻辑错误暴露出来。死锁比较容易debug,把各个线程的调用栈打出来((gdb)threadapplyallbt),只要每个函数不是特别长,很容易看出来是怎么死的。(另一方面支持了函数不要写过长。)或者可以用PTHREAD_MUTEX_ERRORCHECK一下子就能找到错误(前提是MutexLock带debug选项。)
程序反正要死,不如死得有意义一点,让验尸官的日子好过些。
如果一个函数既可能在已加锁的情况下调用,又可能在未加锁的情况下调用,那么就拆成两个函数:
1.跟原来的函数同名,函数加锁,转而调用第2个函数。
2.给函数名加上后缀WithLockHold,不加锁,把原来的函数体搬过来。
就像这样:
void post(const Foo &f)
{
MutexLock Guardlock(mutex);
postWithLockHold(f);//不用担心开销,编译器会自动内联的
}
//引入这个函数是为了体现代码作者的意图,尽管push_back通常可以手动内联
void postWithLockHold(const Foo &f)
{
foos.push_back(f);
}
备注:但,这段代码仍然解决不了上面说的问题。
这有可能出现两个问题:
-
误用了加锁版本,死锁了。
-
误用了不加锁版本,数据损坏了。
Windows互斥量
WaitForSingleObject
获取互斥量Mutex
的返回值可能有三个:WAIT_OBJECT_0
,WAIT_ABANDONED
,WAIT_FAILED
。
-
WaitForSingleObject
,正常获取 -
WAIT_ABANDONED
,一个持有Mutex
的线程退出或者被强制关闭时,没有释放Mutex
。这种情况发生时,系统认为被保护的数据处于一种未知的状态,应该尽可能干净的清理掉这组数据。 -
WAIT_FAILED
,Mutex
已经关闭了,但是另一个程序在试图获取互斥量。即试图获取一个不存在的Mutex
。
保护数据的Mutex
如果一直获取不到:
-
如果
Mutex
进入WAIT_ABANDONED
的状态,能做的就是关闭持有Mutex
的进程,然后重新启动,重新new
一个Mutex
。 -
尽可能的保证获取
Mutex
,处理数据后能够释放掉Mutex
,不论在正常还是异常情况下。
if (false)
{
CloseProcess(hProcessHandle);
}