NIUHE

日々私たちが过ごしている日常というのは、実は奇迹の连続なのかもしれんな

C++笔记

C++这门语言真的是博大精深,在阅读PyTorch源码的时候感触尤为深刻,在此记录一些我认为很好的编程实践以及一些C++的“新”特性。

条件检查

在程序需要判断一些条件才能执行某些操作的时候,我们通常会写如下代码:

1
2
3
4
5
if (cond == true) {
// error, or throw exception
return;
}
do something ...

这些if语句可以简洁为一行代码,如使用assert语句,但如果你不希望程序就此终止的话,也可以自定义相应的处理函数或宏,如果需要检查的条件很多的话,这样进行替换就会使代码整洁许多。下面是在pytorch/c10中的条件判断处理宏(仅供参考):

1
2
3
4
5
6
7
#define AT_ERROR(...) \
throw ::c10::Error({__func__, __FILE__, static_cast<uint32_t>(__LINE__)}, ::c10::str(__VA_ARGS__))

#define AT_CHECK(cond, ...) \
if (!(cond)) { \
AT_ERROR(::c10::str(__VA_ARGS__)); \
}

特性测试

宏定义的运用在C/C++中至关重要,宏可以用来解决一些兼容性的问题,并且使代码保持统一。如constexpr关键字在不同版本的C++中范围不同,那么已知一个函数可以在C++14及以上的版本中声明constexpr但在之前的版本中不行,该怎么办呢?一个既能兼容不同编译器又能保持代码整洁统一的方法就是使用宏定义,如:

1
2
3
4
AT_CPP14_CONSTEXPR const T& front() const {
AT_CHECK(!empty(), "ArrayRef: attempted to access front() of empty list");
return Data[0];
}

AT_CPP14_CONSTEXPR的定义为:

1
2
3
4
5
#if defined(__cpp_constexpr) && __cpp_constexpr >= 201304
# define AT_CPP14_CONSTEXPR constexpr
#else
# define AT_CPP14_CONSTEXPR
#endif

通过特性测试检查特性的指定版本来判断能否使用该关键字,如果版本大于我们的要求,则把宏定义为该关键字,否则定义为空。

使用 nullptr 而不是 NULL

在C++中,宏NULL只是字面量0,而nullptr才是字面量空指针。

函数不要太长

在PyTorch中,函数都很短,一般不超过30行,尽量把函数变得精简。

&&

&& 出现在参数列表中或函数返回值处时表示右值引用(RValue-Reference)。所谓右值引用就是对右值的引用,而右值就是只能出现在等号右边的值,也可以理解为没有内存地址的值。如数字字面量,a+1(int a;) 等都是右值。下面是一个&&用法的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void foo(int&& a)
{
//Some magical code...
}

int main()
{
int b;
foo(b); //Error. An rValue reference cannot be pointed to a lValue.
foo(5); //Compiles with no error.
foo(b+3); //Compiles with no error.

int&& c = b; //Error. An rValue reference cannot be pointed to a lValue.
int&& d = 5; //Compiles with no error.
}

=default 和 = delete

详见:https://www.ibm.com/developerworks/cn/aix/library/1212_lufang_c11new/index.html

简单来说,构造函数、析构函数等特殊函数可以用 =default 来让编译器为其提供默认构造函数或默认析构函数。而 =delete 可以禁用某个函数,如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class X{            
public:
X();
X(const X&) = delete; // 声明拷贝构造函数为 deleted 函数
// 声明拷贝赋值操作符为 deleted 函数
X& operator = (const X &) = delete;
};

int main(){
X x1;
X x2=x1; // 错误,拷贝构造函数被禁用
X x3;
x3=x1; // 错误,拷贝赋值操作符被禁用
}

智能指针

Unique_Ptr

同时只能有一个智能指针对象指向某块内存。

特性:

  1. 无法进行复制构造与赋值操作

    1
    2
    3
    unique_ptr<int> ap(new int(88 );
    unique_ptr<int> one (ap) ; // error
    unique_ptr<int> two = one; // error
  2. 可以进行移动构造和移动赋值

    1
    2
    3
    4
    5
    6
    7
    8
    9
    unique_ptr<int> GetVal() {
    unique_ptr<int> up(new int(88));
    return up;
    }

    unique_ptr<int> uPtr = GetVal(); //ok

    unique_ptr<int> up(new int(88));
    unique_ptr<int> uPtr2 = std:move(up); // ok

Shared_Ptr

shared_ptr是自带引用计数的智能指针,每一个`shared_ptr的拷贝都指向相同的内存。每使用他一次,内部的引用计数加1,每析构一次,内部的引用计数减1,减为0时,删除所指向的堆内存。shared_ptr内部的引用计数是安全的,但是对象的读取需要加锁,阅读更多

Weak_Ptr

weak_ptr 用来表达临时所有权的概念:当某个对象只有存在时才需要被访问,而且随时可能被他人删除时,可以使用 weak_ptr 来跟踪该对象。需要获得临时所有权时,则将其转换为 shared_ptr,此时如果原来的 shared_ptr 被销毁,则该对象的生命期将被延长至这个临时的 shared_ptr 同样被销毁为止。

weak_ptr是为了配合shared_ptr而引入的一种智能指针,它指向一个由shared_ptr管理的对象而不影响所指对象的生命周期,也就是将一个weak_ptr绑定到一个shared_ptr不会改变shared_ptr的引用计数。不论是否有weak_ptr指向,一旦最后一个指向对象的shared_ptr被销毁,对象就会被释放。从这个角度看,weak_ptr更像是shared_ptr的一个助手而不是智能指针。C++中提供了lock函数来实现访问功能。如果对象存在,lock()函数返回一个指向共享对象的shared_ptr,否则返回一个空shared_ptr

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class A
{
public:
A() : a(3) { cout << "A Constructor..." << endl; }
~A() { cout << "A Destructor..." << endl; }

int a;
};

int main() {
shared_ptr<A> sp(new A());
weak_ptr<A> wp(sp);
//sp.reset();

if (shared_ptr<A> pa = wp.lock())
{
cout << pa->a << endl;
}
else
{
cout << "wp指向对象为空" << endl;
}
}

阅读更多

变参模板

变参模板声明:

1
2
template <class... T>
void f(T... args);

省略号的作用有两个:

  1. 声明一个参数包 T… args,这个参数包中可以包含0到任意个模板参数;
  2. 在模板定义的右边,可以将参数包展开成一个一个独立的参数。

基本用法

1
2
3
4
5
6
7
8
9
template <class... T>
void f(T... args)
{
cout << sizeof...(args) << endl; //打印变参的个数
}

f(); //0
f(1, 2); //2
f(1, 2.5, ""); //3

递归展开

1
2
3
4
5
6
7
8
9
10
11
12
template<typename T>
T sum(T t)
{
return t;
}
template<typename T, typename ... Types>
T sum (T first, Types ... rest)
{
return first + sum<T>(rest...);
}

sum(1,2,3,4); //10

初始化列表+逗号表达式展开

1
2
3
4
5
6
7
8
9
10
11
12
13
template <class T>
void printarg(T t)
{
cout << t << endl;
}

template <class ...Args>
void expand(Args... args)
{
int arr[] = {(printarg(args), 0)...};
}

expand(1,2,3,4);

这个例子将分别打印出1,2,3,4四个数字。这种展开参数包的方式,不需要通过递归终止函数,是直接在expand函数体中展开的, printarg不是一个递归终止函数,只是一个处理参数包中每一个参数的函数。

expand函数中的逗号表达式:(printarg(args), 0),先执行printarg(args),再得到逗号表达式的结果0。同时还用到了C++11的另外一个特性——初始化列表,通过初始化列表来初始化一个变长数组, {(printarg(args), 0)…}将会展开成((printarg(arg1),0), (printarg(arg2),0), (printarg(arg3),0), ...),最终会创建一个元素值都为0的数组int arr[sizeof...(Args)]。由于是逗号表达式,在创建数组的过程中会先执行逗号表达式前面的部分printarg(args)打印出参数,也就是说在构造int数组的过程中就将参数包展开了,这个数组的目的纯粹是为了在数组构造的过程展开参数包。

完美转发

1
2
3
4
5
6
7
template<class F, class... Args>
void expand(const F& f, Args&&...args)
{
initializer_list<int>{(f(std::forward<Args>(args)), 0)...};
}
// 范型Lambda表达式
expand([](auto i){cout << i << endl;}, 1, 2.0, ”test”);

std::forward可以保持参数的左值或右值性质,所以叫完美转发。

函数返回值类型后置

例子:

1
2
3
4
5
6
7
8
auto ReadyQueue::push(FunctionTask item) -> void {
{
std::lock_guard<std::mutex> lock(mutex);
++item.base->outstanding_tasks;
heap.push(std::move(item));
}
not_empty.notify_one();
}

解析:https://blog.csdn.net/fjb2080/article/details/7527349

Powered by Hexo and Theme by Hacker
© 2019 NIUHE