C++14、C++17新特性

C++14新特性

  1. 聚合成员初始化 (Aggregate member initialization):这个特性允许你在创建聚合类型(没有用户定义的构造函数、没有私有或保护的非静态数据成员、没有基类和没有虚函数的类)的实例时,直接初始化其成员。
struct Aggregate {
    int a;
    int b;
};

Aggregate aggr{1, 2};  // 初始化 a 为 1,b 为 2
  1. 二进制字面值 (Binary literals):这个特性允许你在代码中直接使用二进制数。
int binary = 0b1010;  // 二进制数 1010 等于十进制数 10
  1. [[deprecated]] 属性:这个特性允许你标记不推荐使用的函数或类型。
[[deprecated("Use newFunction instead")]]
void oldFunction() {
    // ...
}

void newFunction() {
    // ...
}

int main() {
    oldFunction();  // 编译器会发出警告,推荐使用 newFunction
    return 0;
}

//其他地方也能用
namespace [[deprecated]] old_stuff{
    void legacy();
}

enum E {
  foobar = 0,
  foobat [[deprecated]] = 1
};

  1. 数字分隔符 (Digit Separator):这个特性允许你在数字字面值中使用单引号 ' 作为分隔符,以提高可读性。
int million = 1'000'000;  // 等于 1000000,但更易读
  1. 函数返回类型推测 (auto):这个特性允许函数的返回类型根据其返回表达式自动推导,C++11中不支持返回类型推导。
auto add(int a, int b) {
    return a + b;  // 返回类型为 int
}
  1. 通用 Lambda 表达式 (Lambda):这个特性允许 lambda 函数的参数使用 auto 类型,从而实现模板化。
auto genericLambda = [](auto a, auto b) {
    return a + b;
};
  1. 简单的constexpr 功能:这个特性放宽了 constexpr 函数的限制,允许它们包含更多种类的语句。在 C++11 中,constexpr 函数体内只能有一个返回语句,而在 C++14 中,constexpr 函数可以包含更复杂的语句,如条件语句(if-else)和循环语句(for, while)
constexpr int add(int a, int b) {
    return a + b;
}
  1. 变量模板:这个特性允许你定义模板化的变量。
template<typename T>
constexpr T pi = T(3.1415926535897932385);

double circumference(double radius) {
    return 2 * pi<double> * radius;  // 使用 double 类型的 π
    //return 2 * pi<int> * radius;  // 使用 int 类型的 π
}

int main() {
    double r = 1.0;
    std::cout << circumference(r) << std::endl;  // 输出 6.28319
    return 0;
}
  1. std::make_unique (std::make_unique):这个特性引入了 std::make_unique 函数,用于创建 std::unique_ptr 实例。
auto ptr = std::make_unique<int>(5);

这些新特性使得 C++ 更加强大和灵活,也使得编写复杂的 C++ 程序变得更加简单。

  1. shared_timed_mutex

std::shared_timed_mutex 是 C++14 中引入的一个类,它是一个共享定时互斥量。这种互斥量允许多个读者同时拥有锁,但只允许一个写者拥有锁,并且写者在拥有锁的时候不能有读者。

std::shared_lock 是一个配合共享互斥量使用的锁,它允许多个 std::shared_lock 同时拥有一个共享互斥量。

下面是一个使用 std::shared_timed_mutexstd::shared_lock 的示例:

#include <iostream>
#include <thread>
#include <shared_mutex>

std::shared_timed_mutex mutex;
int data = 0;

void reader() {
    std::shared_lock<std::shared_timed_mutex> lock(mutex);
    std::cout << "Reader: " << data << '\n';
}

void writer() {
    std::unique_lock<std::shared_timed_mutex> lock(mutex);
    data++;
    std::cout << "Writer: " << data << '\n';
}

int main() {
    std::thread t1(reader);
    std::thread t2(writer);
    std::thread t3(reader);

    t1.join();
    t2.join();
    t3.join();

    return 0;
}

在这个示例中,reader 函数使用 std::shared_lock 来读取 datawriter 函数使用 std::unique_lock 来写入 data。因为 std::shared_lock 允许多个读者同时拥有锁,所以多个 reader 可以同时读取 data。但是,当 writer 拥有锁的时候,reader 不能读取 data,因为 std::unique_lock 不允许其他锁同时拥有互斥量。

  1. integer_sequence
    std::integer_sequence 是 C++14 中引入的一个模板类,它表示一个编译时的整数序列。

std::integer_sequence 的定义如下:

template<typename T, T... Ints>
class integer_sequence {
    // ...
};

其中,T 是整数类型,Ints 是一个可变参数模板,表示一系列的整数。

例如,std::integer_sequence<int, 0, 1, 2, 3> 表示一个整数序列 0, 1, 2, 3

std::integer_sequence 通常与 std::make_integer_sequencestd::index_sequencestd::make_index_sequence 一起使用。例如:

std::make_integer_sequence<int, 4> // Represents the sequence 0, 1, 2, 3
std::make_index_sequence<4> // Same as std::integer_sequence<std::size_t, 0, 1, 2, 3>

std::integer_sequence 在模板元编程中非常有用,它可以用于生成编译时的整数序列,这些整数序列可以用于索引元组、初始化数组等。

  1. exchange

std::exchange 是 C++14 中引入的一个实用函数,它用于替换一个对象的值,并返回它的旧值。不是一个原子操作

std::exchange 的定义如下:

template< class T, class U = T >
T exchange( T& obj, U&& new_value );

其中,T 是对象的类型,U 是新值的类型。std::exchangeobj 的值替换为 new_value,并返回 obj 的旧值。

下面是一个使用 std::exchange 的示例:

#include <iostream>
#include <utility>

int main() {
    int a = 5;
    int old_a = std::exchange(a, 10);

    std::cout << "Old value of a: " << old_a << '\n';
    std::cout << "New value of a: " << a << '\n';

    return 0;
}

在这个示例中,std::exchangea 的值替换为 10,并返回 a 的旧值 5。所以,输出将是:

Old value of a: 5
New value of a: 10

std::exchange 在需要替换一个对象的值并保留其旧值的情况下非常有用。例如,你可以用它来实现移动赋值运算符。

  1. quoted
    std::quoted 是 C++14 中引入的一个实用函数,它用于生成或解析带引号的字符串。

std::quoted 的定义如下:

template< class CharT, class Traits, class Alloc >
std::basic_string<CharT, Traits, Alloc> quoted( const std::basic_string<CharT, Traits, Alloc>& str, CharT delim = CharT('"'), CharT escape = CharT('\\') );

template< class CharT >
std::basic_string<CharT> quoted( const CharT* str, CharT delim = CharT('"'), CharT escape = CharT('\\') );

其中,CharT 是字符类型,Traits 是字符特性类型,Alloc 是分配器类型。std::quotedstr 包含在 delim 指定的引号中,并使用 escape 指定的转义字符来转义 str 中的特殊字符。

下面是一个使用 std::quoted 的示例:

#include <iostream>
#include <iomanip>

int main() {
    std::string str = "Hello, world!";
    std::cout << std::quoted(str) << '\n';

    return 0;
}

在这个示例中,std::quotedstr 包含在双引号中,所以输出将是:

"Hello, world!"

std::quoted 在需要生成或解析带引号的字符串的情况下非常有用。例如,你可以用它来生成或解析 CSV 文件。

  1. 变量模板

C++ 17

以下是对每个特性的详细解释和示例:

  1. __has_include 预处理说明符:这个特性允许你在预处理时检查一个头文件是否存在。
#if __has_include(<optional>)
# include <optional>
# define have_optional 1
#else
# define have_optional 0
#endif
  1. if constexpr:这个特性允许你在编译时进行条件判断。
template <typename T>
auto get_value(T t) {
    if constexpr (std::is_pointer_v<T>)
        return *t;
    else
        return t;
}
  1. if 和 switch 语句中的初始值设定:这个特性允许你在 if 或 switch 语句的条件部分声明一个变量。类似于for循环的初始化,但必须是声明和初始化
if (auto it = map.find(key); it != map.end()) {
    // 使用 it
}
  1. inline 变量:这个特性允许你在头文件中定义变量,而不会导致多重定义错误。
inline int var = 0;  // 可以在头文件中定义
  1. Fold 表达式:这个特性提供了一种简洁的方式来对参数包进行操作。
template<typename... Args>
auto sum(Args... args) {
    return (... + args);  // 计算所有参数的和
}

//C++11 只能递归展开
template<typename T>
T sum(T v) {
    return v;
}

template<typename T, typename... Args>
T sum(T first, Args... args) {
    return first + sum(args...);
}


//或者使用初始化列表的特性
template<typename... Args>
auto sum(Args... args) -> decltype(args...) {
    typedef decltype(args...) T;
    T result = { 0 };
    int dummy[] = { 0, ((void)(result += args), 0)... };
    (void)dummy;  // 避免未使用变量的警告
    return result;
}
//上面的式子有点复杂,包含逗号表达式,初始化列表,包展开
//在 C++ 中,逗号表达式是一种二元表达式,它的形式是 (E1 , E2)。逗号表达式首先计算左操作数 E1,然后计算右操作数 E2,并返回 E2 的结果作为整个表达式的结果。
//假如args是 1,2,3 整个表达式 int dummy[] = { 0, ((void)(result += args), 0)... }; 会被展开为 int dummy[] = { 0, ((void)(result += 1), 0), ((void)(result += 2), 0), ((void)(result += 3), 0) };。


int main() {
    std::cout << sum(1, 2, 3, 4, 5) << std::endl;  // 输出 15
    return 0;
}

如果参数包是空的,折叠表达式的行为取决于所使用的运算符。对于 && 运算符,折叠的值为 true;对于 || 运算符,折叠的值为 false;对于 , 运算符,折叠的值为 void()

对于其他运算符,如果参数包是空的,折叠表达式是错误的。例如,(... * args) 是错误的,如果 args 是空的。
但是,你可以通过提供一个初始值来避免这个问题。这个初始值被称为 “折叠初始值” 或 “身份元素”。

例如,对于 * 运算符,你可以提供一个初始值 1

template<typename... Args>
auto multiply(Args... args) {
    return (args * ... * 1);  // 这是一个带初始值的折叠表达式
}

在这个例子中,如果 args 是空的,折叠表达式的值将是 1,这是正确的。

对于 -/ 运算符,你可以提供一个初始值 01,但你需要小心处理参数包中的元素的顺序,以避免出现负数或除以零的错误。

对于 &&|| 运算符,你不需要提供初始值,因为它们对空参数包有特殊的处理。

总的来说,处理其他运算符的折叠表达式时,你需要根据运算符的性质和你的需求来选择合适的初始值。

  1. 嵌套命名空间定义:这个特性允许你使用 namespace X::Y 的形式定义嵌套命名空间。
namespace outer {
    namespace inner {
        // 在这里定义函数、类等
    }
}

namespace outer::inner {
    // 在这里定义函数、类等  可以减少代码的缩进层级,使代码更易读
}

  1. 移除了 std::auto_ptr 等:C++17 移除了一些不推荐使用的特性,包括 std::auto_ptrstd::random_shuffle 等。

  2. std::any:这个特性提供了一种类型安全的方式来存储和访问任意类型的值。

#include <any>
#include <iostream>

int main() {
    std::any a = 1;  // 存储一个 int
    std::cout << std::any_cast<int>(a) << '\n';  // 输出:1

    a = std::string("Hello world");  // 存储一个 std::string
    std::cout << std::any_cast<std::string>(a) << '\n';  // 输出:Hello world

    try {
        std::cout << std::any_cast<int>(a) << '\n';  // 尝试将 std::string 转换为 int
    } catch (const std::bad_any_cast& e) {
        std::cout << e.what() << '\n';  // 输出:bad any_cast
    }

    return 0;
}
  1. std::byte:这个特性引入了一个新的整数类型 std::byte,用于表示字节。
std::byte b = std::byte(0xFF);
  1. std::filesystem:这个特性提供了一套用于操作文件系统的 API。

std::filesystem 提供了一系列用于操作文件和目录的函数和类。以下是一些主要的函数和类:

  1. std::filesystem::path:表示文件系统路径的类。

  2. std::filesystem::exists:检查给定路径是否存在。

  3. std::filesystem::create_directory:创建一个新的目录。

  4. std::filesystem::remove:删除一个文件或目录。

  5. std::filesystem::rename:重命名或移动一个文件或目录。

  6. std::filesystem::file_size:获取文件的大小。

  7. std::filesystem::is_directory:检查给定路径是否是一个目录。

  8. std::filesystem::is_regular_file:检查给定路径是否是一个常规文件。

  9. std::filesystem::directory_iterator:一个迭代器,用于遍历目录中的文件和子目录。

以下是一个使用 std::filesystem 的示例:

#include <iostream>
#include <filesystem>

int main() {
    std::filesystem::path p{"C:/Users/username/Documents"};

    if (std::filesystem::exists(p)) {
        std::cout << "Path exists.\n";

        if (std::filesystem::is_directory(p)) {
            std::cout << "Path is a directory.\n";

            // 遍历目录
            for (const auto& entry : std::filesystem::directory_iterator(p)) {
                std::cout << entry.path() << '\n';
            }
        } else if (std::filesystem::is_regular_file(p)) {
            std::cout << "Path is a regular file.\n";
            std::cout << "File size: " << std::filesystem::file_size(p) << " bytes\n";
        }
    } else {
        std::cout << "Path does not exist.\n";
    }

    return 0;
}

在这个例子中,我们首先检查路径 p 是否存在。如果存在,我们检查它是一个目录还是一个常规文件。如果它是一个目录,我们遍历并输出目录中的所有文件和子目录。如果它是一个常规文件,我们输出文件的大小。如果路径 p 不存在,我们输出 “Path does not exist.”。

  1. std::optional:这个特性提供了一种表示可选值的方式。
#include <iostream>
#include <optional>

std::optional<int> get_value(bool condition) {
    if (condition) {
        return 42;  // 返回一个包含值的 std::optional
    } else {
        return std::nullopt;  // 返回一个不包含值的 std::optional
    }
}

int main() {
    auto value1 = get_value(true);
    if (value1.has_value()) {
        std::cout << "Value1: " << value1.value() << '\n';  // 输出:Value1: 42
    }

    auto value2 = get_value(false);
    if (!value2.has_value()) {
        std::cout << "Value2 has no value.\n";  // 输出:Value2 has no value.
    }

    return 0;
}

  1. std::shared_ptr 对 C-类型数组的支持:这个特性允许你使用 std::shared_ptr 管理 C-类型数组。
std::shared_ptr<int> array(new int[10], std::default_delete<int[]>());
    
for (int i = 0; i < 10; ++i) {
    array.get()[i] = i;
}
  1. std::size:这个特性提供了一种获取容器大小的方式。
int arr[] = {1, 2, 3, 4, 5};
auto size = std::size(arr);  // size 为 5
  1. std::string_view:这个特性提供了一种引用字符串的方式,而不需要复制字符串。类似于常量引用,但更灵活 ,可以使用 std::string_view::remove_prefix 和 std::string_view::remove_suffix 来移除视图的前缀和后缀
#include <iostream>
#include <string_view>

void print(std::string_view sv) {
    std::cout << sv << '\n';
}

int main() {
    std::string s = "Hello, world!";
    std::string_view sv = s;

    print(sv);  // 输出:Hello, world!

    sv.remove_prefix(7);
    print(sv);  // 输出:world!

    sv.remove_suffix(1);
    print(sv);  // 输出:world

    return 0;
}
  1. 结构化绑定声明:这个特性允许你一次性声明多个变量。在一些场景很好用,比如函数的返回值,像不像python
#include <iostream>
#include <tuple>

int main() {
    // 从数组中获取值
    int array[3] = {1, 2, 3};
    auto [a, b, c] = array;
    std::cout << a << ' ' << b << ' ' << c << '\n';  // 输出:1 2 3

    // 从元组中获取值
    std::tuple<int, std::string, double> tuple = {4, "Hello", 3.14};
    auto [x, y, z] = tuple;
    std::cout << x << ' ' << y << ' ' << z << '\n';  // 输出:4 Hello 3.14

    // 从结构体中获取值
    struct Point {
        int x;
        int y;
    };
    Point point = {5, 6};
    auto [px, py] = point;
    std::cout << px << ' ' << py << '\n';  // 输出:5 6

    return 0;
}
  1. 构造函数的泛型推测:这个特性允许构造函数进行模板参数推导。
在 C++ 中,构造函数的模板参数不能通过构造函数的参数来推导,这是因为构造函数没有返回类型,所以编译器无法通过返回类型来推导模板参数。这就意味着,如果你有一个模板类,并且你想通过构造函数的参数来创建这个类的实例,你需要显式地指定模板参数。

例如,假设你有一个模板类 `Box`:

```cpp
template <typename T>
class Box {
public:
    Box(T value) : value_(value) {}
private:
    T value_;
};

你不能这样创建 Box 的实例:

Box b(42);  // 错误:无法推导模板参数

你需要这样做:

Box<int> b(42);  // 正确:显式指定模板参数

然而,从 C++17 开始,你可以使用类模板参数推导(Class Template Argument Deduction, CTAD)来让编译器自动推导模板参数。这意味着你可以这样创建 Box 的实例:

Box b(42);  // 正确:使用类模板参数推导

编译器会自动推导 Tint 类型。这使得代码更简洁,更易于阅读。


17. **typename 可以在模板参数中使用**:这个特性允许你在模板参数中使用 `typename` 关键字。

```cpp
template <typename T>
class MyClass {};

template <template <typename> typename C>
class AnotherClass {};

AnotherClass<MyClass> ac;

在模板参数中使用 typename 关键字用于指定一个类型参数。在示例中,AnotherClass 是一个模板类,它接受一个模板类 C 作为参数,而这个模板类 C 又接受一个类型参数。

这种模式通常被称为模板模板参数,它允许你传递一个模板作为另一个模板的参数。这是一种非常强大的特性,可以使代码更加灵活和通用。

在示例中,AnotherClass<MyClass> ac; 这行代码创建了一个 AnotherClass 的实例,它的模板参数 C 被设置为 MyClass。这意味着 AnotherClass 可以使用 MyClass 来创建对象或执行操作。

  1. UTF-8 字符字面值:这个特性允许你使用 UTF-8 字符字面值。可以避免改变文件编码导致的错误,在老项目里容易遇到,老项目对于这种喜欢直接用16进制的宏定义,可读性不好
char utf8[] = u8"Hello, world!";
  1. static_assert:是 C++11 引入的一种特性,允许你在编译时进行断言检查。如果 static_assert 的条件为 false,编译器将发出一个错误,并显示你提供的错误消息。

这是一个 static_assert 的例子:

static_assert(sizeof(int) == 4, "This code requires int to be 4 bytes.");

在这个例子中,如果 int 的大小不是 4 字节,编译器将发出一个错误,错误消息是 “This code requires int to be 4 bytes.”。

static_assert 非常有用,它可以帮助你确保你的代码的某些假设在所有平台和编译器配置上都是正确的。例如,你可以使用 static_assert 来检查某个类型的大小,或者检查某个模板参数是否满足你的要求。

在 C++17 中,static_assert 有了一些改进。你现在可以省略错误消息,如果你这样做,编译器将生成一个默认的错误消息。例如:

static_assert(sizeof(int) == 4);

在这个例子中,如果 int 的大小不是 4 字节,编译器将发出一个默认的错误消息。

  1. *Lambda捕获 this

在 C++17 中,Lambda 表达式增加了一个新的捕获方式,可以使用 *this 来捕获当前对象的副本。这在你希望在 Lambda 表达式中使用当前对象的成员时非常有用,而且你不希望 Lambda 表达式持有对当前对象的引用。

例如,考虑以下类:

class MyClass {
    int value = 123;

public:
    auto getLambda() {
        return [*this] { return value; };
    }
};

在这个例子中,getLambda 方法返回一个 Lambda 表达式,该表达式捕获了 *this 的副本。这意味着,即使 MyClass 对象在 getLambda 被调用后被销毁,返回的 Lambda 表达式仍然可以安全地访问 value 成员。

注意,如果你使用 this 而不是 *this 来捕获当前对象,Lambda 表达式将持有对当前对象的引用。这可能会导致问题,如果 MyClass 对象在 getLambda 被调用后被销毁,返回的 Lambda 表达式将持有一个悬挂引用,这将导致未定义行为。*this 返回的是一个副本,隐式捕获 用到了才捕获,没有用到就不会去捕获,拷贝副本

  1. 过度对齐支持

如果你需要对动态内存进行过度对齐(over-alignment),你可以使用 std::aligned_allocoperator new。这两种方法都可以创建一个指定对齐要求的动态内存块。

以下是使用 std::aligned_alloc 的例子:

#include <cstdlib>

int main() {
    // 分配一个 256 字节的内存块,对齐到 128 字节边界
    void* ptr = std::aligned_alloc(128, 256);
    if (ptr == nullptr) {
        // 处理内存分配失败
    }

    // 使用内存...

    std::free(ptr);  // 不要忘记释放内存
    return 0;
}

以下是使用 operator new 的例子(需要 C++17 或更高版本):

#include <new>

int main() {
    // 分配一个 256 字节的内存块,对齐到 128 字节边界
    void* ptr = ::operator new(256, std::align_val_t(128));
    if (ptr == nullptr) {
        // 处理内存分配失败
    }

    // 使用内存...

    ::operator delete(ptr, std::align_val_t(128));  // 不要忘记释放内存
    return 0;
}

注意,过度对齐可能会增加内存使用和分配成本。在使用过度对齐时,你需要确保你的代码能够正确处理内存分配失败的情况,并且在不再需要内存时释放它。

在 C++17 之前,new 表达式不保证能满足超过 alignof(std::max_align_t) 的对齐要求。但在 C++17 中,这个问题得到了解决,new 表达式现在可以正确地处理过度对齐的类型。

所以,你的代码在 C++17 或更高版本的编译器上应该可以正常工作:

class alignas(16) float4 {
    float f[4];
};

int main() {
    float4 *p = new float4[1000];

    // 使用 p...

    delete[] p;  // 不要忘记释放内存
    return 0;
}

这段代码会创建一个包含 1000 个 float4 对象的动态数组,每个 float4 对象都对齐到 16 字节边界。

如果你使用的是 C++17 之前的编译器,你需要使用 std::aligned_alloc 或者其他平台特定的函数来分配内存,然后使用定位 new 表达式来构造对象。但这种方法比较复杂,也更容易出错,所以如果可能的话,最好升级到 C++17 或更高版本的编译器。

  1. 类模板参数推导:在 C++17 中,引入了类模板参数推导(Class Template Argument Deduction, CTAD),这使得我们在实例化类模板时,可以省略模板参数,编译器会自动推导出这些参数。

例如,对于 std::pair,我们可以这样写:

std::pair p(1, 2.0);  // 自动推导为 std::pair<int, double>

编译器会根据构造函数的参数类型来推导模板参数。在上面的例子中,编译器看到 std::pair 的构造函数接受一个 int 和一个 double,所以它推导出模板参数为 <int, double>

你也可以为你自己的类模板提供推导指引(Deduction Guide),这是一种特殊的函数模板,它告诉编译器如何从构造函数的参数类型推导出模板参数。例如:

template<typename T>
class MyArray {
    // ...
};

// 推导指引
template<typename T, std::size_t N>
MyArray(T (&)[N]) -> MyArray<T>;

这个推导指引告诉编译器,如果 MyArray 的构造函数接受一个数组,那么模板参数 T 应该是数组元素的类型。这样我们就可以这样写:

int arr[10];
MyArray a(arr);  // 自动推导为 MyArray<int>

需要注意的是,类模板参数推导不能用于所有情况。在某些情况下,编译器可能无法推导出正确的模板参数,或者推导出的模板参数可能不是你期望的。在这些情况下,你仍然需要手动指定模板参数。

  1. "保证复制省略:这个特性可以消除某些情况下的对象复制和移动操作,从而提高性能。

具体来说,当一个函数返回一个局部对象,或者从函数返回一个临时对象,这个对象的复制或移动操作可以被省略。编译器直接在目标位置构造这个对象,而不是先在函数内部构造然后再复制或移动到目标位置。

例如,考虑以下代码:

class BigObject {
    // ...
};

BigObject createBigObject() {
    return BigObject();
}

int main() {
    BigObject obj = createBigObject();
    return 0;
}

在 C++17 之前,createBigObject 函数会先在栈上创建一个 BigObject 对象,然后复制或移动这个对象到 main 函数中的 obj。这个复制或移动操作可能会消耗大量的时间和资源,特别是对于大型对象。

但在 C++17 中,由于保证复制省略,编译器会直接在 main 函数中的 obj 的位置上构造 BigObject 对象,没有任何复制或移动操作。这可以大大提高性能,特别是对于大型对象。

需要注意的是,保证复制省略只适用于特定的情况。在其他情况下,对象的复制或移动可能仍然不能被省略。你可以查阅 C++ 标准或相关文档,了解更多关于保证复制省略的详细信息。
23. 继承构造函数

在 C++11 之前,构造函数是不能被继承的。这意味着,如果你有一个基类和一个派生类,派生类不会自动获得基类的构造函数。你需要在派生类中手动定义构造函数,如果需要,还要手动调用基类的构造函数。

例如,考虑以下代码:

class Base {
public:
    Base(int) { }
};

class Derived : public Base {
public:
    Derived(int x) : Base(x) { }  // 必须手动调用基类的构造函数
};

在 C++11 中,引入了继承构造函数的特性。这个特性允许派生类继承基类的构造函数。具体来说,当你在派生类中使用 using 声明基类的构造函数时,编译器会为派生类生成一组新的构造函数,这些构造函数接受与基类构造函数相同的参数,并将这些参数转发给基类构造函数。

例如:

class Base {
public:
    Base(int) { }
    Base(double, double) { }
};

class Derived : public Base {
public:
    using Base::Base;  // 继承基类的构造函数
};

int main() {
    Derived d1(42);        // 调用 Base::Base(int)
    Derived d2(3.14, 2.72);  // 调用 Base::Base(double, double)
    return 0;
}

在这个例子中,Derived 类继承了 Base 类的所有构造函数。因此,你可以使用 Base 类的构造函数来构造 Derived 类的对象。

需要注意的是,继承构造函数的行为与其他 using 声明有一些不同。其他 using 声明只是使基类的成员在派生类中可见,而继承构造函数实际上是为派生类生成了新的构造函数。这些构造函数会接受与基类构造函数相同的参数,并将这些参数转发给基类构造函数。

此外,如果派生类有自己的成员变量,这些成员变量会被默认初始化(也就是说,它们的默认构造函数会被调用)。如果这些成员变量没有默认构造函数,你需要在派生类中提供一个合适的构造函数,而不能依赖继承的构造函数。

然而,在 C++17 中,这个特性的行为发生了变化。现在,继承构造函数的行为更接近于其他 using 声明。具体来说,当你在派生类中使用 using 声明基类的构造函数时,基类的构造函数会在派生类中变得可见,就像它们是派生类的构造函数一样。这意味着,你不再需要为派生类生成新的构造函数,而是可以直接使用基类的构造函数。

例如:

class Base {
public:
    Base(int) { }
    Base(double, double) { }
};

class Derived : public Base {
public:
    using Base::Base;  // 继承基类的构造函数
};

int main() {
    Derived d1(42);        // 调用 Base::Base(int)
    Derived d2(3.14, 2.72);  // 调用 Base::Base(double, double)
    return 0;
}

在这个例子中,Derived 类继承了 Base 类的所有构造函数。因此,你可以使用 Base 类的构造函数来构造 Derived 类的对象。

此外,如果派生类有自己的成员变量,这些成员变量会被默认初始化(也就是说,它们的默认构造函数会被调用)。这是 C++17 中继承构造函数行为的一个重要改变。在 C++11 和 C++14 中,如果派生类的成员变量没有默认构造函数,你需要在派生类中提供一个合适的构造函数,而不能依赖继承的构造函数。但在 C++17 中,这不再是必需的。

  1. 强类型枚举
    在 C++11 中引入的强类型枚举(也称为枚举类)允许你指定一个基础类型。基础类型决定了枚举值的存储方式和大小。你可以使用任何整数类型作为基础类型,包括 charshortintlonglong long 以及它们的无符号版本。

例如,你可以这样定义一个具有 uint32_t 基础类型的枚举类:

enum class Handle : uint32_t { Invalid = 0 };

然后,你可以使用直接列表初始化来创建一个 Handle 类型的变量:

Handle h { 42 };  // OK

在这个例子中,42 是一个 uint32_t 类型的值,它被直接赋值给 h 变量。这是因为 Handle 的基础类型是 uint32_t,所以你可以使用任何 uint32_t 类型的值来初始化 Handle 类型的变量。

需要注意的是,即使枚举类有一个基础类型,你仍然不能直接将枚举值转换为整数,也不能与整数进行比较。如果你需要将枚举值转换为整数,你可以使用静态类型转换:

uint32_t value = static_cast<uint32_t>(h);

这是强类型枚举的一个重要特性,它可以帮助你避免一些类型转换的错误。

  1. 更严格的表达式顺序
    在 C++17 中,表达式的评估顺序变得更加严格。在 C++17 之前,表达式的评估顺序在很大程度上是未定义的。这意味着,如果一个表达式中有多个子表达式,那么这些子表达式的评估顺序是不确定的。

例如,考虑以下代码:

int x = f() + g();

在 C++17 之前,f()g() 的调用顺序是不确定的。f() 可能先被调用,也可能是 g() 先被调用。

然而,在 C++17 中,这个问题得到了解决。现在,表达式的评估顺序更加严格。具体来说,函数调用的参数现在是从左到右依次评估的。这意味着,在上面的例子中,f() 一定会在 g() 之前被调用。

这个改变使得代码的行为更加可预测,也减少了因为评估顺序不确定而导致的错误。然而,这也意味着,你需要更加小心地编写代码,以确保你的代码不会依赖于特定的评估顺序。

  1. 基于范围的不同开始和结束类型
    在 C++17 中,基于范围的 for 循环的语义进行了一些修改,以支持开始迭代器和结束迭代器类型不同的情况。这是一个重要的改进,因为它允许更广泛的使用场景,特别是对于一些自定义的迭代器类型。

在 C++17 之前,基于范围的 for 循环的语义大致如下:

{
   auto && __range = for_range_initializer;
   for ( auto __begin = begin_expr,
              __end = end_expr;
              __begin != __end;
              ++__begin ) {
        for_range_declaration = *__begin;
        statement
   }
}

在这个模型中,__begin__end 必须是相同的类型,因为它们都是用 auto 声明的。

然而,在 C++17 中,这个模型被修改为:

{
  auto && __range = for_range_initializer;
  auto __begin = begin_expr;
  auto __end = end_expr;
  for ( ; __begin != __end; ++__begin ) {
    for_range_declaration = *__begin;
    statement
  }
}

在这个新模型中,__begin__end 可以是不同的类型。唯一的要求是,__begin 类型的对象必须能够与 __end 类型的对象进行比较。

这个改变使得基于范围的 for 循环更加灵活,可以处理更多的情况。例如,你可以定义一个迭代器类型,它的 begin() 方法返回一个类型,而 end() 方法返回一个不同的类型。只要这两种类型的对象可以进行比较,你就可以在基于范围的 for 循环中使用它们。

  1. ** using 声明中使用参数包扩展**:
    在 C++17 中,引入了在 using 声明中使用参数包扩展的能力。这是一个重要的改进,因为它使得代码更加简洁,更易于理解。

在 C++17 之前,如果你想从可变参数模板中的所有基类公开 operator(),你需要使用递归的方式:

template <typename T, typename... Ts>
struct Overloader : T, Overloader<Ts...> {
    using T::operator();
    using Overloader<Ts...>::operator();
    // […]
};

template <typename T> struct Overloader<T> : T {
    using T::operator();
};

在这个例子中,Overloader 模板继承自所有的 Ts 类型,并且从每个基类中引入 operator()。这需要使用递归,因为在 C++17 之前,using 声明不能包含参数包扩展。

然而,在 C++17 中,你可以在 using 声明中使用参数包扩展:

template <typename... Ts>
struct Overloader : Ts... {
    using Ts::operator()...;
    // […]
};

在这个新的例子中,Overloader 模板仍然继承自所有的 Ts 类型,但是现在可以在一个 using 声明中引入所有基类的 operator()。这使得代码更加简洁,也更易于理解。

  1. 十六进制浮点文字
    在 C++17 中,引入了十六进制浮点文字,这是一个重要的改进,因为它使得表示某些特殊的浮点值变得更加容易。

十六进制浮点文字的形式为 0x0X 开头,后跟一个十六进制数,然后是 pP,最后是一个十进制的指数。十六进制数表示浮点数的尾数,指数表示尾数的二进制指数。

例如,0x1.0p-126 是一个十六进制浮点文字。它表示的是 1.0 * 2^-126,这是最小的正常 IEEE-754 单精度值。

这是一个使用十六进制浮点文字的示例:

#include <iostream>

int main() {
    float f = 0x1.0p-126;
    std::cout << f << '\n';
    return 0;
}

在这个示例中,f 被初始化为 0x1.0p-126,这是最小的正常 IEEE-754 单精度值。然后,这个值被打印出来。

  1. 当前线程中未捕获的异常对象的数量
    std::uncaught_exceptions() 是一个在 C++17 中引入的函数,它返回当前线程中未捕获的异常对象的数量。这在实现作用域保护时非常有用,特别是在堆栈展开期间。

下面是一个使用 std::uncaught_exceptions() 的示例,该示例中的类 ScopeGuard 在构造函数中记录未捕获的异常数量,然后在析构函数中检查这个数量是否有变化:

#include <iostream>
#include <exception>

class ScopeGuard {
public:
    ScopeGuard() : exceptionCount(std::uncaught_exceptions()) {}

    ~ScopeGuard() {
        if (std::uncaught_exceptions() > exceptionCount) {
            std::cout << "Destructor called due to an exception.\n";
        } else {
            std::cout << "Destructor called normally.\n";
        }
    }

private:
    int exceptionCount;
};

int main() {
    try {
        ScopeGuard guard;
        throw std::runtime_error("An exception");
    } catch (const std::exception& e) {
        std::cout << "Caught exception: " << e.what() << '\n';
    }

    return 0;
}

在这个示例中,如果 ScopeGuard 的析构函数是由于异常而被调用的,那么它会打印 “Destructor called due to an exception.”。如果析构函数是正常调用的,那么它会打印 “Destructor called normally.”。

  1. ** SFINAE**:
    这是一个使用 SFINAE (Substitution Failure Is Not An Error) 技术的示例。SFINAE 是 C++ 模板元编程中的一个重要概念,它允许在编译期间选择最合适的模板。

在这个示例中,定义了两个 get_value 函数模板,它们的选择取决于模板参数 T 是否是算术类型。

template <typename T, std::enable_if_t<std::is_arithmetic<T>{}>* = nullptr>
auto get_value(T t) {/*...*/}

这个函数模板只接受算术类型的参数。如果 T 是算术类型(如 intfloat 等),那么 std::is_arithmetic<T>{} 将返回 truestd::enable_if_t<std::is_arithmetic<T>{}>* 将是一个有效的类型,所以这个函数模板将是有效的。

template <typename T, std::enable_if_t<!std::is_arithmetic<T>{}>* = nullptr>
auto get_value(T t) {/*...*/}

这个函数模板只接受非算术类型的参数。如果 T 不是算术类型,那么 !std::is_arithmetic<T>{} 将返回 truestd::enable_if_t<!std::is_arithmetic<T>{}>* 将是一个有效的类型,所以这个函数模板将是有效的。

因此,这两个函数模板可以根据参数的类型自动选择最合适的一个。例如,如果你调用 get_value(123),那么将选择第一个函数模板,因为 123 是一个算术类型。如果你调用 get_value("hello"),那么将选择第二个函数模板,因为 "hello" 不是一个算术类型。

  1. 标签派发
    标签派发(Tag Dispatching)技术是一种在编译时根据类型选择最合适的函数的技术。

下面的 get_value 函数模板,它们的选择取决于第二个参数的类型。

template <typename T>
auto get_value(T t, std::true_type) {/*...*/}

这个函数模板接受一个 std::true_type 类型的参数。std::true_type 是一个空的结构体,它有一个静态成员 value,其值为 true

template <typename T>
auto get_value(T t, std::false_type) {/*...*/}

这个函数模板接受一个 std::false_type 类型的参数。std::false_type 是一个空的结构体,它有一个静态成员 value,其值为 false

template <typename T>
auto get_value(T t) {
    return get_value(t, std::is_arithmetic<T>{});
}

这个函数模板根据 T 是否是算术类型来选择上面的两个函数模板中的一个。如果 T 是算术类型,那么 std::is_arithmetic<T>{} 将返回一个 std::true_type 类型的对象,所以将选择第一个函数模板。如果 T 不是算术类型,那么 std::is_arithmetic<T>{} 将返回一个 std::false_type 类型的对象,所以将选择第二个函数模板。

因此,这三个函数模板可以根据参数的类型自动选择最合适的一个。例如,如果你调用 get_value(123),那么将选择第一个函数模板,因为 123 是一个算术类型。如果你调用 get_value("hello"),那么将选择第二个函数模板,因为 "hello" 不是一个算术类型。

等等等等,这上面就是最常用的特性了

废弃的特性

  1. 三字母
    “三字母序列”(Trigraphs)是 C++ 中的一种特性,它允许你使用三个字符的序列来表示一些不能在所有键盘布局上直接输入的字符。例如,??= 表示 #??( 表示 [??) 表示 ],等等。

    然而,这个特性在实践中很少使用,而且可能会引起混淆,因此在 C++17 中,三字母序列被移除了。

    这意味着在 C++17 及以后的版本中,你不再需要(也不能)使用三字母序列。如果你的代码中包含三字母序列,你需要将它们替换为对应的字符。

  2. register 关键字
    在 C++17 中,register 关键字已被弃用。在早期的 C++ 和 C 语言中,register 关键字用于提示编译器,该变量会频繁使用,应尽可能地将其存储在 CPU 寄存器中以提高访问速度。然而,现代的编译器优化技术已经足够智能,可以自动决定哪些变量应该存储在寄存器中,因此 register 关键字已经没有实际用途了。

    如果你的代码中还有使用 register 关键字的地方,你应该删除它。这不会影响你的代码的性能,因为现代编译器会自动进行寄存器分配。

    例如,如果你的代码中有这样的声明:

    register int x = 0;
    

    你应该将其改为:

    int x = 0;
    

    如果你的代码中有很多使用 register 关键字的地方,你可以使用文本编辑器的查找和替换功能,或者使用脚本来自动进行替换。

  3. 删除bool类型的 ++操作
    在 C++17 中,对 bool 类型的 operator++ 已被弃用。在早期的 C++ 版本中,你可以对 bool 类型的变量使用 ++ 运算符,但这种用法并不直观,可能会导致混淆,因此在 C++17 中被弃用了。

  4. 旧的异常规范
    在 C++17 中,旧的异常规范(也被称为 “动态异常规范”)已被弃用,并被新的 “noexcept 规范” 替代。

    在早期的 C++ 版本中,你可以使用 throw 关键字来指定一个函数可能抛出的异常类型:

    void old_style() throw(int) {
        // 这个函数可能会抛出 int 类型的异常
    }
    

    在 C++17 中,你应该使用 noexcept 关键字来指定一个函数是否会抛出异常:

    void new_style() noexcept {
        // 这个函数保证不会抛出任何异常
    }
    

    如果你的代码中有使用旧的异常规范的地方,你应该将其替换为 noexcept 规范。如果你的函数可能会抛出异常,你可以省略 noexcept 关键字:

    void might_throw() {
        // 这个函数可能会抛出异常
    }
    

    在 C++17 中,异常规范已经成为了类型系统的一部分。这意味着,一个函数的异常规范(即该函数是否标记为 noexcept)现在是该函数类型的一部分。

    这对于模板编程尤其重要,因为你现在可以在模板参数中使用 noexcept 来约束函数参数:

    template <typename F>
    void call_noexcept_function(F f) noexcept(is_nothrow_invocable_v<F>) {
        f();
    }
    

    在这个例子中,call_noexcept_function 函数接受一个函数 f 作为参数,并调用它。noexcept(is_nothrow_invocable_v<F>) 是一个异常规范,它指定 call_noexcept_function 是否为 noexcept,取决于 f 是否为 noexcept

    这个特性使得你可以编写更安全的代码,因为你现在可以在编译时检查函数是否可能抛出异常,并据此做出决策。例如,你可以选择只调用 noexcept 函数,或者在调用可能抛出异常的函数时使用 try/catch 块。

附录

参数展开包

参数包展开操作符 ... 在 C++ 中可以在多种上下文中使用,不仅仅是在初始化列表中。以下是一些使用参数包展开操作符的例子:

  1. 函数调用:在函数调用中,可以使用 ... 来展开参数包并将参数传递给函数。

    template<typename... Args>
    void func(Args... args) {
        otherFunc(args...);  // 展开参数包并将参数传递给 otherFunc
    }
    
  2. 初始化列表:在初始化列表中,可以使用 ... 来展开参数包并将参数用于初始化列表。

    template<typename... Args>
    void func(Args... args) {
        std::vector<int> v{ args... };  // 展开参数包并将参数用于初始化列表
    }
    
  3. 类型列表:在类型列表中,可以使用 ... 来展开参数包并将类型用于继承列表或模板参数列表。

    template<typename... Args>
    struct MyTuple : std::tuple<Args...> {  // 展开参数包并将类型用于继承列表
    };
    
  4. 折叠表达式:在 C++17 中,可以使用 ... 来创建折叠表达式,这是一种对参数包中的所有元素执行相同操作的方式。

    template<typename... Args>
    auto sum(Args... args) {
        return (args + ...);  // 展开参数包并将参数用于折叠表达式
    }
    

这些都是参数包展开操作符 ... 的使用方式,它们都是在处理变长参数列表时的有用工具。

属性

在 C++ 中,属性是由编译器、标准库或者用户定义的。C++ 标准定义了一些属性,但编译器和库也可以定义自己的属性。以下是一些 C++ 标准定义的属性:

  • [[nodiscard]]:表示函数的返回值不应被忽略。
  • [[noreturn]]:表示函数不会返回。
  • [[maybe_unused]]:表示变量、参数、函数或类可能不会被使用,编译器不应发出未使用的警告。
  • [[likely]][[unlikely]]:表示某个条件分支的可能性(C++20 引入)。
  • [[deprecated]]:表示函数、变量或类型已经被弃用。
  • [[fallthrough]] 属性 指示 switch 语句中的 fallthrough 是故意的,不应为其发出警告。

编译器和库也可以定义自己的属性。例如,GCC 定义了 [[gnu::const]][[gnu::pure]][[gnu::hot]] 等属性,用于优化代码。这些属性的具体含义和用法取决于定义它们的编译器或库。

注意,不同的编译器和库定义的属性可能不兼容。在使用非标准的属性时,你需要确保你的代码在目标平台的编译器和库上能够正确工作。

属性可以用于多种语言元素,包括类型、函数、变量、类成员、枚举、模板等。以下是一些使用属性的例子:

  1. 函数属性:
[[nodiscard]] int foo() {
    return 42;
}

int main() {
    foo();  // 编译器会警告,因为 foo() 的返回值被忽略了
    return 0;
}
  1. 变量属性:
int main() {
    [[maybe_unused]] int x = 42;  // 编译器不会警告 x 未被使用
    return 0;
}
  1. 类成员属性:
class MyClass {
    [[deprecated]] void old_method() {}  // 这个方法已经被弃用
};
  1. 枚举属性:
enum [[deprecated]] OldEnum {  // 这个枚举已经被弃用
    VALUE1, VALUE2
};
  1. 模板属性:
template<typename T>
[[deprecated]] void old_template_function(T value) {}  // 这个模板函数已经被弃用

注意,不同的属性有不同的使用规则和限制。在使用属性时,你需要查阅相关文档,确保你正确地使用了属性。

在 C++ 中,属性可以有命名空间,这主要是为了避免不同的库或者编译器定义的属性之间的命名冲突。属性的命名空间通常是定义该属性的库或者编译器的名称。

例如,GCC 编译器定义的属性使用 gnu 命名空间,如 [[gnu::const]][[gnu::pure]] 等。这意味着这些属性是 GCC 特有的,可能不被其他编译器支持。

同样,一些库也可以定义自己的属性,并使用自己的命名空间。例如,你提到的 rpr 可能是一个库定义的命名空间,[[rpr::kernel]][[rpr::target(cpu,gpu)]] 是这个库定义的属性。

注意,C++ 标准定义的属性没有命名空间,如 [[nodiscard]][[noreturn]] 等。这些属性应该被所有遵循 C++ 标准的编译器支持。

在 C++17 中,对于编译器不支持的属性命名空间,标准规定了应该忽略它们。这是一个重要的改进,因为它允许开发者在代码中使用特定编译器的特性,而不会破坏其他编译器的兼容性。
例如,你可以这样定义一个特定编译器的属性:

[[MyCompilerSpecificNamespace::do_special_thing]]
void foo();

在这个例子中,do_special_thing 是一个属性,它属于 MyCompilerSpecificNamespace 命名空间。如果编译器支持这个属性,它会对 foo 函数进行特殊处理。如果编译器不支持这个属性,根据 C++17 的规定,它会忽略这个属性,而不是产生错误。这个特性使得开发者可以在保持代码兼容性的同时,利用特定编译器的特性。

一个复杂的模板

#include <iostream>

// 定义两个函数对象
struct Foo {
    void operator()(int i) const {
        std::cout << "Foo: " << i << '\n';
    }
};

struct Bar {
    void operator()(const char* s) const {
        std::cout << "Bar: " << s << '\n';
    }
};

// Overloader 的定义
template <typename T, typename... Ts>
struct Overloader : T, Overloader<Ts...> {
    using T::operator();
    using Overloader<Ts...>::operator();
};

template <typename T>
struct Overloader<T> : T {
    using T::operator();
};

int main() {
    Overloader<Foo, Bar> overloader;

    // 调用 Foo 的 operator()
    overloader(123);

    // 调用 Bar 的 operator()
    overloader("hello");

    return 0;
}

在这个示例中,Overloader<Foo, Bar>FooBar 的子类,并且它引入了 FooBaroperator()。因此,你可以使用 overloader 对象来调用 FooBaroperator()

这段代码定义了一个名为 Overloader 的模板结构体,它可以从多个基类中继承,并且可以将这些基类的 operator() 引入到自己的作用域中。

让我们逐行解析这段代码:

template <typename T, typename... Ts>

这是一个模板声明,它声明了一个模板参数 T 和一个模板参数包 TsT 是一个类型,Ts 是一个类型的集合,可以包含任意数量的类型。

struct Overloader : T, Overloader<Ts...> {

这是 Overloader 结构体的定义。Overloader 继承自 TOverloader<Ts...>。这意味着 OverloaderT 的所有成员,并且还有 Overloader<Ts...> 的所有成员。这是一个递归的定义,因为 Overloader 是自己的基类。

using T::operator();
using Overloader<Ts...>::operator();

这两行代码将 TOverloader<Ts...>operator() 引入到 Overloader 的作用域中。这意味着,如果 TOverloader<Ts...> 有一个 operator() 成员,那么 Overloader 也会有这个成员。

这个结构体的目的是创建一个类型,它可以从多个类型中继承,并且可以将这些类型的 operator() 引入到自己的作用域中。这在某些情况下是非常有用的,例如,当你需要创建一个函数对象,它可以接受多种不同类型的参数,并且对每种类型的参数都有不同的行为。

  • 57
    点赞
  • 42
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值