Loading

编码实践总结

工具

---
# .clang-format
# This configuration requires clang-format version 6.0 exactly.
BasedOnStyle: WebKit
---
Language: Cpp
ColumnLimit: 90
...

模板

模板的声明和实现最好都在头文件中,隐式实例化。

除非,模板特化,显式实例化。

特化

函数模板特化:

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

书籍

列表:

多线程

互斥锁的使用

尽量用非递归互斥量,避免使用递归互斥量

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

备注:但,这段代码仍然解决不了上面说的问题。

这有可能出现两个问题:

  1. 误用了加锁版本,死锁了。

  2. 误用了不加锁版本,数据损坏了。

Windows互斥量

WaitForSingleObject获取互斥量Mutex的返回值可能有三个:WAIT_OBJECT_0WAIT_ABANDONEDWAIT_FAILED

  • WaitForSingleObject,正常获取

  • WAIT_ABANDONED,一个持有Mutex的线程退出或者被强制关闭时,没有释放Mutex。这种情况发生时,系统认为被保护的数据处于一种未知的状态,应该尽可能干净的清理掉这组数据。

  • WAIT_FAILEDMutex已经关闭了,但是另一个程序在试图获取互斥量。即试图获取一个不存在的Mutex

保护数据的Mutex如果一直获取不到:

  • 如果Mutex进入WAIT_ABANDONED的状态,能做的就是关闭持有Mutex的进程,然后重新启动,重新new一个Mutex

  • 尽可能的保证获取Mutex,处理数据后能够释放掉Mutex,不论在正常还是异常情况下。

if (false)
{
    CloseProcess(hProcessHandle);
}
posted @ 2020-12-16 11:31  hfang  阅读(166)  评论(0)    收藏  举报