语言可用性增强

语言可用性增强

fetch150zy

关于C++(Modern)中对语言可用性的强化

语言可用性的强化

常量

nullptr VS NULL

nullptr出现的目标是为了替代NULL,传统C++会把NULL,0视为同一种东西,这取决于编译器如何定义NULL,有些编译器会把NULL定义为((void*)0),有些则会直接将其定义为0

而在C++中不允许直接将void *隐式转换到其他类型

:kissing: example

1
2
3
4
void foo(char *);
void foo(int);
// will call foo(int)
foo(NULL)

C++11引入nullptr关键字,专门用来区分空指针和0;nullptr的类型为nullptr_t,能够隐式的转换为任何指针或成员指针的类型

constexpr

const常数不等同于常量表达式

:worried: example

1
2
const int len = 1 + 1;
char arr[len]; // invalid

上面这种行为在大多数编译器中都支持,但是这是一个非法的行为

tips: 现在大部分编译器都带有自身编译优化,很多非法行为在编译器的优化的加持下会变得合法

C++11提供了constexpr让用户显式的声明函数或对象构造函数在编译期会成为常量表达式,这个关键字明确的告诉编译器应该去验证常量表达式

constexpr修饰的函数可以使用递归:

:grinning: example

1
2
3
constexpr int fibonacci(const int n) {
return n == 1 || n == 2 ? 1 : fibonacci(n - 1) + fibonacci(n - 2);
}

C++14开始,constexpr函数可以在内部使用局部变量,循环和分支等简单语句

:grinning: example

1
2
3
4
5
6
// C++14起可以通过编译
constexpr int fibonacci(const int n) {
if (n == 1) return 1;
if (n == 2) return 2;
return fibonacci(n - 1) + fibonacci(n - 2);
}

变量及其初始化

if/switch变量声明强化

C++17起,可在if和switch语句中声明一个临时变量

:astonished: example

1
2
3
if (const auto iter = std::find(vec.begin(), vec.end(), 3); iter != vec.end()) { 
...
}

初始化列表

在传统C++中,不同对象有着不同的初始化方法;对于数组和POD类型都可以使用{}进行初始化,而对于类对象的初始化,要么需要拷贝构造、要么就需要使用()进行

为了解决这个问题,C++11把初始化列表的概念绑定到类型上std::initializer_list,允许构造函数像其他函数参数一样使用初始化列表,为类对象的初始化与普通数组和POD的初始化方法提供统一的桥梁

:weary: example

1
2
3
4
5
6
class Foo {
public:
Foo(std::initializer_list<T> list);
};
// use by this
Foo foo = {....};

结构化绑定

结构化绑定提供了类似其他语言中提供的多返回值的功能

C++11/14给我们提供了std::tuple来构造一个元组,进而囊括多个返回值;但并未提供简单的方式从元组中拿到并定义元组中的元素(使用std::tie对元组拆包);C++17给出了结构化绑定

:yum: example

1
2
3
4
5
std::tuple<int, double, std::string> f() {
return std::make_tuple(1, 2.3, "456");
}

auto [x, y, z] = f();

类型推导

C++11引入了autodecltype实现类型推导

auto

tips: auto很早就存在于C++,但始终作为一个存储类型的指示符,与register并存

C++20起,auto甚至能用于函数传参

:confused: example

1
2
3
int add(auto x, auto y) {
return x + y;
}

decltype

decltype关键字是为了解决auto关键字只能对变量进行类型推导的缺陷而出现的,类似于typeof(非C++标准)

:sunglasses: example

1
2
3
4
decltype(expr);
// can be used to compare type
if (std::is_same<decltype(x), decltype(y)>::value)
...

尾返回类型推导

在传统C++中我们必须这么做:

1
2
3
4
template<typename R, typename T, typename U>
R add(T x, U y) {
return x + y;
}

C++11起这个问题得以解决:

1
decltype(x + y) add(T x, U y);

但是这种写法并不能通过编译,C++11引入尾返回类型(trailing return type),利用auto关键字将返回类型后置

:hushed: example

1
2
3
4
template<typename T, typename U>
auto add(T x, U y) -> decltype(x + y) {
return x + y;
}

C++14起可以直接让普通函数具备返回值推导,下面这种写法也就合法了

1
2
3
4
template<typename T, typename U>
auto add(T x, U y) {
return x + y;
}

decltype(auto)

涉及参数转发,decltype(auto)主要用于对转发函数或封装的返回值类型进行推导,使得我们无需显式指定decltype的参数表达式

example

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
std::string lookup1();
std::string& lookup2();

// in C++11
std::string lookup_string_1() {
return lookup1();
}
std::string& lookup_string_2() {
return lookup2();
}
// by decltype(auto) in C++14
decltype(auto) lookup_string_1() {
return lookup1();
}
decltype(auto) lookup_string_2() {
return lookup2();
}

控制流

if constexpr

C++11引入constexpr关键字,将表达式或函数编译为常量结果,将其加入到条件判断中让代码在编译期就完成分支判断

:smirk: example

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
template<typename T>
auto print_type_info(const T& t) {
if constexpr (std::is_integral<T>::value) {
return t + 1;
} else {
return t + 0.01;
}
}
// 在编译时,实际代码如下
int print_type_info(const int& t) {
return t + 1;
}
double print_type_info(const double& t) {
return t + 0.01;
}

区间for迭代

C++11引入基于范围的迭代写法

1
2
3
4
5
6
// in c++ 17
std::array data = {"hello", ",", "world"};
std::size_t i = 0;
for (auto & d: data) {
std::cout << i++ << ' ' << d << '\n';
}
1
2
3
4
// in c++ 20
for (std::size_t i = 0; auto const & d: data) {
std::cout << i++ << ' ' << d << '\n';
}

模板

模板的哲学在于将一切能够在编译期处理的问题丢到编译期进行处理,仅在运行时处理那些最核心的动态服务,进而大幅优化运行期的性能

外部模板

在传统C++中,模板只有在使用时才会被编译期实例化:只要在每个编译单元中编译的代码中遇到被完整定义的模板,都会实例化(产生了重复实例化而导致的编译时间增加,并且没有办法通知编译器不要触发模板的实例化)

C++11引入外部模板,扩充了原来的强制编译器在特定位置实例化模板的语法,使得我们能够显式的通知编译器何时进行模板的实例化

1
2
template class std::vector<bool>;		// 强制实例化
extern template class std::vector<double>; // 不在当前编译文件中实例化模板

类型别名模板

模板是用来产生类型的,在传统C++中,typedef可以为类型定义一个新的名称,但是却没有办法为模板定义一个新的名称

C++11使用using引入了下面这种形式的写法,并且同时支持对传统typedef相同的功效

1
using NewName = OldName<xxx, xxx>;

变长参数模板

C++11加入了新的表示方法,允许任意个数、任意类别的模板参数,同时也不需要在定义时将参数的个数固定

1
template<typename... Ts> class Magic;

模板类Magic的对象,能够接受不受限制个数的typename作为模板的形式参数(包括0个参数)

使用变长参数模板实现变长参数函数

1
template<typename... Args> void printf(const std::string &str, Args... args);

对参数解包

:smirk: example

1
2
3
4
template<typename... Ts>
void magic(Ts... args) { // 使用sizeof...获取参数个数
std::cout << sizeof...(args) << std::endl;
}
  1. 递归模板函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    template<typename T0>
    void printf(T0 value) {
    std::cout << value << std::endl;
    }
    template<typename T, typename... Ts>
    void printf(T value, Ts... args) {
    std::cout << value << std::endl;
    printf1(args...);
    }
  2. 变参模板展开

    C++17中增加了变参模板展开的支持

    1
    2
    3
    4
    5
    6
    template<typename T0, typename... T>
    void printf(T0 t0, T... t) {
    std::cout << t0 << std::endl;
    if constexpr (sizeof...(t) > 0)
    printf(t...);
    }
  3. 初始化列表展开

    黑魔法(利用了std::initializer_list强制顺序求值的特性)

    1
    2
    3
    4
    5
    6
    7
    template<typename T, typename... Ts>
    auto printf(T value, Ts... args) {
    std::cout << value << std::endl;
    (void) std::initializer_list<T>{([&args] {
    std::cout << args << std::endl;
    }(), value)...};
    }

折叠表达式

1
2
3
4
template<typename... T>
auto sum(T... t) {
return (t + ...);
}

非类型模板参数推导

使用不同字面量作为模板参数

:satisfied: example

1
2
3
4
5
6
7
8
9
10
template<typename T, int BufSize>
class buffer_t {
public:
T& alloc();
void free(T& item);
private:
T data[BufSize];
};
// use
buffer_t<int, 100> buf;

tips: 也可使用auto来进行类型推导

1
2
3
4
template<auto value> void foo() {
....
}
foo<10>(); // value被推导为int类型

面向对象

final keyword

C++11引入:限制类的继承以及阻止成员函数的覆盖

  • 阻止类被继承

    1
    2
    class Base final { };
    class Derived : public Base { /* 编译错误:Base 是一个 final 类,不能被继承 */ };
  • 阻止派生类中覆盖方法

    1
    2
    3
    4
    5
    6
    7
    8
    class Base {
    public:
    virtual void foo() final { /* foo 函数不能在派生类中被覆盖 */ }
    };
    class Derived : public Base {
    public:
    void foo() override { /* 编译错误:foo 被声明为 final,不能被覆盖 */ }
    };

static members

类的静态成员是与类本身相关联的成员,而不是与类的各个实例相关联的

静态成员变量

  1. 共享:静态成员变量为类的所有对象所共享。无论创建了多少个类的实例,都只有一个静态成员变量的拷贝
  2. 独立于任何对象:即使没有创建类的实例,静态成员变量也存在,它们与类类型自身关联,而不是与特定的实例关联
  3. 初始化:静态成员变量需要在类定义外进行初始化(通常在源文件中),对于整型或枚举类型的常量静态成员,可以在类内直接初始化
  4. 访问:静态成员变量可以通过类名和作用域解析运算符 :: 访问,也可以通过类的对象或引用访问

静态成员函数

  1. 独立于对象:静态成员函数不依赖于类的任何特定实例。它们不能访问类的非静态成员变量,也不能调用非静态成员函数
  2. 全局访问:静态成员函数可以在不创建类的实例的情况下调用
  3. 访问限制:静态成员函数遵循正常的访问控制规则(publicprotectedprivate

:confused: example

1
2
3
4
5
6
7
8
9
10
class MyClass {
public:
static int staticVar; // 静态成员变量声明

static void staticFunc() { // 静态成员函数
// 可以访问静态成员变量,但不能访问非静态成员变量
}
};

int MyClass::staticVar = 0; // 静态成员变量定义和初始化

静态成员变量通常用于存储与类相关的信息,如对象计数器或类特定的常量值

静态成员函数常用于实现与类的具体实例无关的功能,如工厂方法或辅助函数

构造函数

Rule of 3/5

:astonished: Good practice: The rule of 3/5(C++98/C++11)

  • Rule of three

    如果你的类需要自定义析构函数、拷贝构造函数或拷贝赋值操作符中的任何一个,那么你很可能需要自定义所有这三个

    这是因为自定义析构函数通常意味着你的类在管理(如分配和释放)资源,这通常需要自定义的拷贝控制来正确地复制或赋值这些资源

  • Rule of five

    规则五包括规则三中的三个函数(析构函数、拷贝构造函数、拷贝赋值操作符)以及两个新的函数:移动构造函数和移动赋值操作符

    如果你需要自定义析构函数,你很可能也需要自定义所有五个函数,以正确处理移动语义,从而提高效率

构造函数中的初始化列表

:+1: 成员初始化列表提供的值优先于类定义处的默认值

成员初始化列表直接初始化成员变量,而不是先默认初始化然后赋值;对于某些类型(如引用和常量成员),这是必需的,因为它们不能被赋值

其次就是在效率方面:对于非内置类型的成员,使用初始化列表通常比在构造函数中赋值更高效,避免了额外的构造和赋值

默认构造函数

  • 定义:如果一个构造函数可以不带任何参数被调用(或者所有参数都有默认值),则称为默认构造函数

  • 作用:在不提供任何初始化值的情况下创建对象

  • 示例:

    1
    2
    3
    4
    5
    class Example {
    public:
    Example() = default; // 保持了特殊成员函数的自动生成:比如拷贝构造函数,析构函数等
    Example() { } // 破坏了类的聚合类型属性,可能会阻止编译器生成其他特殊成员函数
    };

行为:

  1. 对于内置类型的成员:默认构造函数不会初始化这些成员,它们的初始值是未定义的,除非它们被显示初始化
  2. 对于类类型的成员:这些成员的默认构造函数将被调用来初始化它们
  3. 对于继承的情况:如果类是从其他类继承的,基类的默认构造函数将被调用
  4. 对于带有默认成员初始化的成员:在C++11及更高版本中,可以在类定义中直接给成员变量赋初值,如果这样做了,即使是默认的构造函数,这些成员也会被初始化为指定的值

参数化构造函数

  • 定义:带有一个或多个参数的构造函数,用于根据提供的参数初始化对象

  • 作用:提供更灵活的初始化方式,允许在创建对象时指定初始值

  • 示例:

    1
    2
    3
    4
    class Example {
    public:
    Example(...args) { /* 构造函数体 */ }
    };

行为:

  1. 实现类型转换:如果参数化构造函数没有被声明为explicit,它还可能用于隐式类型转换

拷贝构造函数

  • 定义:参数为对同类型对象的引用(通常为常量引用)的构造函数

  • 作用:用于通过复制另一个同类型对象来初始化新对象

  • 示例:

    1
    2
    3
    4
    class Example {
    public:
    Example(const Example & other) { /* 拷贝构造函数体 */ }
    };

行为:

  1. 创建对象副本:拷贝构造函数通过接收一个同类型对象的引用作为参数来创建新对象的副本。这个过程包括复制现有对象所有的成员变量的值
  2. 被隐式调用:当函数通过值传递或者函数返回对象时,拷贝构造函数会被隐式调用

移动构造函数(C++11引入)

  • 定义:参数为同类型对象的右值引用的构造函数

  • 作用:允许资源的转移,提高效率,尤其用于临时对象

  • 示例:

    1
    2
    3
    4
    class Example {
    public:
    Example(Example && other) { /* 移动构造函数体 */ }
    };

行为:

  1. 参数类型:移动构造函数接受一个同类型对象的右值引用作为参考
  2. 资源转移:与拷贝构造函数(进行深拷贝)不同,移动构造函数设计用来窃取源对象的资源。这意味着它通过将资源从源对象转移到新创建的对象,来实现快速构造,而不是复制资源
  3. 优化临时对象:移动构造函数特别适用于临时对象或将要销毁的对象,因为这些情况下资源转移是安全的。比如在函数返回临时对象或进行对象赋值时,移动构造函数可以显著提高效率
  4. 自动生成规则:如果一个类没有自定义拷贝构造函数、拷贝赋值操作符、移动赋值操作符以及析构函数,编译器可能会自动生成移动构造函数

委托构造函数(C++11引入)

定义:一个构造函数在其初始化列表中调用同一类的另一个构造函数

  • 作用:简化多个构造函数的代码,避免重复

  • 示例:

    1
    2
    3
    4
    5
    6
    7
    8
    class Example {
    public:
    Example(int x): m_x(x) {}
    Example(int x, int y): Example(x), m_y(y) {} // 委托构造函数和成员初始化列表不能同时存在
    Example(int x, int y): Example(x) { this->m_y = y; } // ok
    private:
    int m_x, m_y;
    };

行为:

  1. 减少重复代码:委托构造函数可以减少在多个构造函数中重复相同的初始化代码。这有助于提高代码的可维护性和一致性
  2. 初始化顺序:在委托构造函数中,首先执行被调用的构造函数,然后才执行调用构造函数的主体部分
  3. 限制:不能在同一个构造函数中同时使用委托构造函数和成员初始化列表
  4. 使用场景:适合于那些有多个构造函数,且这些构造函数之间有共同初始化逻辑

显式构造函数

  • 定义:使用 explicit 关键字标记的构造函数

  • 作用:防止隐式类型转换和意外的构造函数调用

  • 示例:

    1
    2
    3
    4
    class Example {
    public:
    explicit Example(...) { /* 构造函数体 */ }
    };

隐式构造函数

  • 定义:未用 explicit 关键字标记的构造函数
  • 作用:允许编译器在需要时自动调用该构造函数进行类型转换

删除的构造函数(C++11引入)

  • 定义:使用 delete 关键字明确禁止的构造函数

  • 作用:防止生成默认的构造函数或禁止某些类型的对象构造

  • 示例:

    1
    2
    3
    4
    class Example {
    public:
    Example() = delete;
    };

继承的构造函数(C++11引入)

  • 定义:子类可以使用 using 语句继承基类的构造函数

  • 作用:允许子类继承基类的构造函数,简化代码

  • 示例:

    1
    2
    3
    4
    5
    6
    7
    class Base {
    public:
    Base(...) {}
    };
    class Derived: public Base {
    using Base::Base;
    }

行为:

  1. 自动转发参数:当使用基类的构造函数创建派生类对象时,构造函数的参数会被自动转发给基类的对应构造函数

不支持函数重载

关于委托构造函数和直接初始化的一些细节:

在需要代码重用和保持初始化逻辑一致时有限考虑委托构造函数

构造函数初始化逻辑和独立且简单时优先考虑直接初始化

强类型枚举

传统C++中枚举类型并非安全类型,枚举类型会被视为整数(甚至同一个命名空间中的不同枚举类型的枚举值名字不能相同)

C++11引入枚举类

1
2
3
4
5
enum class new_enum : unsigned int { // 未指定枚举值类型时默认为int
value1,
value2,
value3 = 100
};

不能被隐式的转换为整数,同时也不能够将其与整数数字进行比较

:wink: nice code (获取枚举值)

1
2
3
4
5
6
7
template<typename T>
std::ostream& operator<<(
typename std::enable_if(std::is_enum<T>::value),
std::ostream>::type& stream, const T& e)
{
return stream << static_cast<typename std::underlying_type<T>::type>(e);
}