在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
#include
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直接构造容器内元素的方法,同时也提供了()其他功能的实现,很好地解决了这个问题。
在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
#include
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总是合理的,而且是必须的!
使用别名声明和typedef在很多情况下都是同样简单的。
using value_type = pair<string, int>;
typedef pair<string, int> value_type1;
但如果是用于函数指针的话,别名会简单少许。
using func = void(*)(int, string);
typedef void(*Func)(int, string);
这都不是最重要的优势。最重要的优势在于使用模板时。所以很多时候别名声明也叫别名模板。
#include
#include
using namespace std;
template <typename T>
using MyList = list ;
template <typename T>
struct MyList1 {
typedef list type;
};
template <typename T>
class test {
public:
typename MyList1::type value1;
MyList value;
};
int main(int argc, char *argv[]) {
MyList<int> a;
MyList1<int> b;
return 0;
}
如果使用typedef,每次在别的模板里使用时都要加上typename,用来告诉编译器,这是一个类型定义。但是如果使用using的别名声明,则不需要这么麻烦。因为编译器已经知道这是一个别名!于是乎在使用过程中就可以很简单地运用。
这样的理由足以说服你使用using了吧。
在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(person) << endl;
cout << get(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(person) << endl;
cout << get(person) << endl;
return 0;
}
虽然这看起来还是比用C++98的enum需要更多的代码,但为了强类型和作用域的问题,还是得忍啊。
在以前,如果我们希望某个函数不被调用,最可行的方法是把他放在私有部分并且只给声明,不给定义。而现在在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:
};
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函数,有许多的要求:
什么是引用标识符呢?
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
#include
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,如果不是会给出报错。当然了,如果你非常仔细,当然不会出现什么问题,但一旦出错,你可能要花很长的时间才能找到错误的来源。另外,如果你想要改变基类函数的实现,那么他的子函数会立即给出报错,从而极快地定为需要修改的部分。
在C++98中,很多容器的成员函数是不接受const_iterator作为参数的,因为他们没有这样的重载函数,但是在C++11和C++14中,问题得到了解决。容器提供了相应的迭代器,以及相应的函数实现版本。
#include
#include
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
#include
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);
}
缺省的构造函数会按照数据成员(非static)一个个的复制相应的值,如果你希望它是这样完成的,并且不愿意做出大量的实现代码,就可以使用这个标识符。这个标识符甚至可以用在右值引用中。
#include
#include
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;
}
编译器在一定的情况下给出相应的特殊的成员函数: