[C++]高效使用c++11的一些建议

Modern C++

1. 区别() 和 {}

在C++11之后,初始化的方法多了用{}的方法。而这种方法几乎是所有初始化方法中最普遍适用,但只是几乎。

C++98不允许类的数据成员声明时被初始化,但现在可以了。

class test {
public:
    test() {
    }
private:
    int x0 = 0; // int x0(0); error!
    int x1{0};
    int x2 = {0};
};

甚至对于atomic这样不可以复制的对象也可以被{}初始化。

还有在{}(braced initializer)中还不允许出现内置类型(built-in types)的隐式类型转换。(implicit narrowing conversion )。

    int t = {1.0 + 2.0 + 3}; // error! can't truncated to an int.

C++总是会尽量把声明理解为函数调用(most vexing parse),而使用{}则可以避免这种情况发生。

int main(int argc, char *argv[]) {
    test a(); 
    // warning: empty parentheses interpreted as a function declaration [-Wvexing-parse]
        test a();
    test b{};
    return 0;
}

我们前面还提到{}并不是完美的。之前的文章提到auto在推导{}的类型时,会把他理解为initializer_list。同样的,在这里也是一样的。如果{}有机会理解为initializer_list,它一定会这么做,除非无法实现类型转换匹配它的类型要求。

class test {
public:
    test(int a) {
        cout << "int" << endl;
    }
    test(double b) {
        cout << "double" << endl;
    }
    test(initializer_list<bool> li) {
        cout << "List" << endl;
    }
};
int main(int argc, char *argv[]) {
    test a(3); // call the first ctor
    test b(3.0); // call the second ctor
    test c{1}; // call the third ctor.
    test c{1.0}; // error!
    return 0;
}

如果你希望添加的一个空的initializer_list, 必须这么写:

#include <iostream>
#include <list>
using namespace std;
class test {
public:
    test(int a) {
        cout << "int" << endl;
    }
    test(double b) {
        cout << "double" << endl;
    }
    test(initializer_list<bool> li) {
        cout << "List" << endl;
    }
    test() {
        cout << "default" << endl;
    }
    operator double() {
        cout << "convert" << endl;
        return 1.0;
    }
};
int main(int argc, char *argv[]) {
    test a{}; // call default!
    test b{{}}; // call list
    test c({}); // ditto
    return 0;
}

因为{}有以上的特性,所以我们在写类实现时需要注意构造函数的写法,特别是在实现了initializer_list为参数的构造函数,需要顾及用户在使用时的想法。参考vector的interface,他提供了initializer_list直接构造容器内元素的方法,同时也提供了()其他功能的实现,很好地解决了这个问题。

2. 使用nullptr而不是null 和 0

在C++中, Null和0几乎是一样的。但是nullptr是nullptr_t, 它可以直接隐式类型转换为任意指针类型,但!绝不会转换为integer。

在c++98时,人们总是尽量避免让int的参数类型的函数有指针的参数类型的重载,因为这样指针的重载类型会有很大的限制。因为Null会被理解为0!

void f(int a) {
    cout << "int" << endl;
}
void f(void* ptr) {
    cout << "pointer" << endl;
}

int main(int argc, char *argv[]) {
    f(0);
    f(NULL); 
    // error: call to 'f' is ambiguous
    f(nullptr);
    return 0;
}

编译器很友善地提出调用f函数会产生歧义。这就是因为NULL可以被隐式类型转换为0,而且总是会这么做。

还有一个原因就是,代码的清晰性(clarity)。因为NULL和0几乎是一样的,所以用0来做判断指针是否有指向并没有问题,但是对于可读性会有很大的问题,因为你无法判断ptr的类型究竟是int 还是 pointer!

#include <iostream>
#include <list>
using namespace std;
template <typename T>
decltype(auto) create() {
    T* ptr = NULL;
    return ptr;
}
int main(int argc, char *argv[]) {
    auto ptr = create<int>();
    if (ptr == 0) {
        cout << "It is int!" << endl;
    }
    if (ptr == nullptr) {
        cout << "It is pointer!" << endl;
    }
    return 0;
}

所以指针指向空时使用nullptr总是合理的,而且是必须的!

3. 使用别名声明而不是typedef

使用别名声明和typedef在很多情况下都是同样简单的。

using value_type = pair<string, int>;
typedef pair<string, int> value_type1;

但如果是用于函数指针的话,别名会简单少许。

using func = void(*)(int, string);
typedef void(*Func)(int, string);

这都不是最重要的优势。最重要的优势在于使用模板时。所以很多时候别名声明也叫别名模板。

#include <iostream>
#include <list>
using namespace std;

template <typename T>
using MyList = list<T>;

template <typename T>
struct MyList1 {
    typedef list<T> type;
};
template <typename T>
class test {
public:
    typename MyList1<T>::type value1;
    MyList<T> value;
};
int main(int argc, char *argv[]) {
    MyList<int> a;
    MyList1<int> b;
    return 0;
}

如果使用typedef,每次在别的模板里使用时都要加上typename,用来告诉编译器,这是一个类型定义。但是如果使用using的别名声明,则不需要这么麻烦。因为编译器已经知道这是一个别名!于是乎在使用过程中就可以很简单地运用。

这样的理由足以说服你使用using了吧。

4. 使用有作用域的enum!

在c++98时,enum内元素的作用域和enum的作用域是一样的。也就是说在enum的作用域内,enum里定义的元素都不能再被作为变量名。

int main() {
  enum color {
    white, balck, blue
  };
  auto white = 10; //  namespace pollution!
  return 0;
}

但是C++11中给出了有作用域的enum类型。

int main() {
  enum class color {
    white, black, blue
  };
  auto white = 10;  // right!
  auto white = color::white; // right!
  return 0;
}

C++11中的enum类型还是强类型的,不存在隐式类型转换。
只能使用强制类型转换来调整。

int main() {
  enum class color {
    white, black, blue
  };
  auto w = color::white;
//  cout << w << endl;  //  error!
  cout << static_cast<int>(w) << endl;
  return 0;
}

但是C++98的enum则可以隐式类型转换。

int main() {
  enum color {
    white, black, blue
  };
  cout << white << endl;
  return 0;
}

C++98的enum不可以预先声明,但是C++11的则可以。(forward declaration)

enum class colors;
enum color; // error!
int main() {
  return 0;
}
enum color {
  white, black, blue
};
enum class colors {
  white, black, blue
};

还有一点需要注意的!在C++98中,enum中只要有一个元素被修改,那么所有使用了这个enum的代码都需要重新编译!就算没用使用那个修改的元素。但是在C++11中,只要没用使用那个被修改的元素就可以不必重新编译。

基于C++11的强类型和强作用域的特性,的确值得被使用,但还有一种情况是C++98更加好用。那就是需要使用enum的自动类型转换的时候。

假设我们需要一个tuple,我们不可能一直记得tuple里面的元素是怎么定义的,只能希望用一个enum来表示tuple里面元素定义的原因。例如:

using value_type = tuple<string, int, string>;
enum UserInfo {
  Name, Age, PhoneNumber
};
int main() {
  value_type person{"yan", 20, "123456"};
  cout << get<Name>(person) << endl;
  cout << get<Age>(person) << endl;
  return 0;
}

这显得更加的直接,但如果使用C++11,则不那么显然了。

using value_type = tuple<string, int, string>;
enum class UserInfo {
  Name, Age, PhoneNumber
};
int main() {
  value_type person{"yan", 20, "123456"};
  cout << get<static_cast<int>(UserInfo::Name)>(person) << endl;
  cout << get<static_cast<int>(UserInfo::Age)>(person) << endl;
  return 0;
}

当然也可以自定义一个函数来完成这种转换。

using value_type = tuple<string, int, string>;
enum class UserInfo {
  Name, Age, PhoneNumber
};
template <typename T>
constexpr auto ToUType(T enums) noexcept {
  return static_cast<int>(enums);
}
int main() {
  value_type person{"yan", 20, "123456"};
  cout << get<ToUType(UserInfo::Name)>(person) << endl;
  cout << get<ToUType(UserInfo::Age)>(person) << endl;
  return 0;
}

虽然这看起来还是比用C++98的enum需要更多的代码,但为了强类型和作用域的问题,还是得忍啊。

5. 不希望被使用的函数要定义为delete

在以前,如果我们希望某个函数不被调用,最可行的方法是把他放在私有部分并且只给声明,不给定义。而现在在C++11中,我们可以更直接地说明某个函数不能被调用,就是把他设为delete!

bool isluckly(int number) {
  return number == 1;
}

int main() {
  cout << isluckly(10) << endl;
  cout << isluckly(1.0) << endl;
  cout << isluckly(true) << endl;
  cout << isluckly('a') << endl;
  return 0;
}

只要能够转换为int的参数都可以直接调用isluckly!!

以前的解决方法是只声明,不定义。

bool isluckly(int number) {
  return number == 1;
}
bool isluckly(double);
bool isluckly(bool);
bool isluckly(char);

测试中给出的错误报告,非常费解。属于链接错误的问题。
但如果使用delete,则简单了很多。

bool isluckly(int number) {
  return number == 1;
}
bool isluckly(double) = delete;
bool isluckly(bool) = delete;
bool isluckly(char) = delete;
int main() {
  cout << isluckly(10) << endl;
  cout << isluckly(1.0) << endl;
  cout << isluckly(true) << endl;
  cout << isluckly('a') << endl;
  return 0;
}

这样会给出非常明确的错误报告,说明调用了被delete的函数。
值得注意的是,我们只声明了double参数的重载函数不能被调用,但没有声明float,是因为float类型在转换的时候更自然地会转型为double而不是int,所以声明double类型就足够了。

还有一种必须使用delete的情形。

在C++中,有两种指针是非常特别的。一种是void*, 他不能被解析,不能自增,不能自减!另一种是char *类型,这种指针直接表示c-style的string,而不是指向单个char。所以我们在声明需要指针为参数的模板函数时,很有可能需要把这两种类型的特化版本delete掉。

template <typename T>
void toPtr(T* ptr) {
  cout << *ptr << endl;
}
template <>
void toPtr(void* ptr) = delete;
template <>
void toPtr(const void* ptr) = delete;
template <>
void toPtr(char* ptr) = delete;
template <>
void toPtr(const char* ptr) = delete;

int main() {
  int a = 10;
  auto temp = &a;
  toPtr(temp);
  void* t = &a;
  toPtr(t);
  return 0;
}

错误信息会明确给出调用void*的特化版本是不被允许的。特别地,既然void 不可以被调用,当然const void 也应该不可以被调用。

直到现在还是可以使用只声明,不定义的方法啊!

但是接下来,问题来了,如果是在类的声明中呢?

class Widget {
public:
  template <typename T>
  void Process(T* ptr) {
    cout << *ptr << endl;
  }
private:
  template<>
  void Process(void*);
};

如果使用这种方法则是不行的!因为模板特化只能写在命名空间的作用域内,而不是类的作用域内。所以你应该这么写:

class Widget {
public:
  template <typename T>
  void Process(T* ptr) {
    cout << *ptr << endl;
  }
private:
};
template <>
void Widget::Process<void>(void*);

这样就可以实现无法调用了!但问题还是错误报告难以理解。

如果我们写出delete的,则可以给出更加易懂的错误报告。

class Widget {
public:
  template <typename T>
  void Process(T* ptr) {
    cout << *ptr << endl;
  }
private:
};
template <>
void Widget::Process<void>(void*) = delete;

最后一点需要说明的是,我们一般都愿意把被删除的函数放在public而不是private,原因在于,如果把函数放在private,错误报告只会说,你调用了私有部分的函数,而不是告诉你这个函数被delete了。所以更合适的做法,是把他放在public部分,这样,一旦调用了这个函数,就会直接给出清晰的报错,更能触及错误的本质。

template <typename T>
class Widget {
public:
  void Process(T* ptr) {
    cout << *ptr << endl;
  }
  void Process(void* ptr) = delete;
private:
};

6. 声明override函数要写出override

C++是一个OOP的语言,这意味着继承,多态几乎是C++最重要的特性。实际上写出override函数并不难,但是很容写错,而且编译器不会认为是错误,于是这很有可能会导致许多难以察觉的错误。

class Base {
public:
  virtual void test(int a) {
    cout << a << endl;
  }
};
class Derived : public Base {
public:
  void test(int a) override {
    cout << a << endl;
  }
};

声明为override函数,有许多的要求:

    1. 基类函数必须是virtual.
    1. 函数名必须是一样的
    1. 参数类型必须是一样的。
    1. const属性必须是一样的。
    1. 返回类型和抛出异常的类型必须是可兼容的(compatiable)
    1. 函数的引用标识符(reference qualifier)也必须是一样的。

什么是引用标识符呢?

class Base {
 public:
  void test(int a) & { cout << "left value" << endl; }
    // if *this is left value reference
  void test(int a) && { cout << "right value" << endl; }
    // if *this is right value reference.
};
int main() {
  Base a;
  a.test(10);
  move(a).test(10);
  return 0;
}

这个部分需要继续讨论一下。

引用标识符能够充分利用临时对象的资源,因为当没有这种语法时,我们并不知道*this是不是一个临时对象,如此,我们就无法利用右值引用的转移语义来实现对右值的运用。

#include <iostream>
#include <vector>
using namespace std;
class Base {
 public:
  using value_type = vector<int>;
  Base(initializer_list<int> orig) {
    data.insert(data.end(), orig.begin(), orig.end());
  }
  value_type& pass() & { return data; }
  value_type&& pass() && {
    cout << "moved" << endl;
    return move(data);
  }

 private:
  value_type data;
};
int main() {
  Base a{1, 2, 3, 4, 5};
  Base::value_type data = a.pass();
  for (auto vec : data) {
    cout << vec << endl;
  }
  Base::value_type data1 = Base{1, 2, 3, 4}.pass();
  for (auto vec : data1) {
    cout << vec << endl;
  }
  return 0;
}

我们再继续原来话题。多写一个override的一个最大的好处就在于,他会明确告诉你,这个函数是不是override,如果不是会给出报错。当然了,如果你非常仔细,当然不会出现什么问题,但一旦出错,你可能要花很长的时间才能找到错误的来源。另外,如果你想要改变基类函数的实现,那么他的子函数会立即给出报错,从而极快地定为需要修改的部分。

7. 在必要时使用const_iterator

在C++98中,很多容器的成员函数是不接受const_iterator作为参数的,因为他们没有这样的重载函数,但是在C++11和C++14中,问题得到了解决。容器提供了相应的迭代器,以及相应的函数实现版本。

#include <iostream>
#include <vector>
using namespace std;
int main() {
  vector<int> vec{1, 2, 3, 4, 5};
  auto it = find(vec.cbegin(), vec.cend(), 3);
  cout << *it << endl;
  return 0;
}

但是更广泛可用的代码是使用相应的非成员函数。

#include <iostream>
#include <vector>
using namespace std;
int main() {
  vector<int> vec{1, 2, 3, 4, 5};
  string str{"yan"};
  auto it = find(cbegin(str), cend(str), 'a');
  cout << *it << endl;
  return 0;
}

实际上cbegin()的实现是很容易的。

template <typename T>
decltype(auto) cbegin(const T& container) {
  return std::begin(container);
}

8. 当你希望某些成员函数以默认形式实现时,加上default

缺省的构造函数会按照数据成员(非static)一个个的复制相应的值,如果你希望它是这样完成的,并且不愿意做出大量的实现代码,就可以使用这个标识符。这个标识符甚至可以用在右值引用中。

#include <iostream>
#include <vector>
using namespace std;
class test {
public:
  test(test& orig) {
    a = 10;
    cout << "Copy assignment!" << endl;
  }
  test() = default;
  test& operator=(const test& orig) = default;
  int a;
};
int main() {
  test a;
  a.a = 1;
  test b(a);
  b = a;
  cout << b.a << endl;
  return 0;
}

编译器在一定的情况下给出相应的特殊的成员函数:

    1. 缺省构造函数:类中没有自定义的构造函数。
    1. 析构函数:缺省情况下是default,并且只有在基类是virtual时为virtual。
    1. 拷贝构造函数:当类中没有自定义的拷贝构造函数。当实现了move操作,则不实现。
    1. 拷贝操作符:同上。
    1. move构造函数和move赋值操作:当类中没有用户定义的拷贝操作,移动操作,或析构函数。
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值