现代C++14 交换

文章目录

std::swap

std::swap 是 C++ 标准库中的一个通用函数,用于交换两个对象的值。它最初定义在头文件 <algorithm> 中,但从 C++11 开始,它的主要定义被移动到了头文件 <utility> 中,而数组的特化版本仍然可以在 <algorithm> 中找到。此外,某些特定类型的 std::swap 特化可能在其他头文件中定义,例如 <string_view>

1. std::swap 的基本用法

std::swap 的基本形式如下:

template <class T>
void swap(T& a, T& b);

这个模板函数接受两个引用参数 ab,并交换它们的值。std::swap 的行为是通过调用 T 类型的移动构造函数和移动赋值运算符来实现的,因此要求 T 必须满足 std::is_move_constructible_v<T>std::is_move_assignable_v<T>

2. std::swap 的异常安全性

  • noexcept 规范:从 C++11 开始,std::swap 可以被标记为 noexcept,具体取决于 T 的移动构造和移动赋值操作是否是 noexcept 的。如果这些操作是 noexcept 的,那么 std::swap 也可以是 noexcept 的。

    noexcept(
        std::is_nothrow_move_constructible_v<T> &&
        std::is_nothrow_move_assignable_v<T>
    )
    
  • constexpr 支持:从 C++20 开始,std::swap 可以在常量表达式中使用,即它可以被标记为 constexpr

3. 数组的 std::swap 特化

对于数组,std::swap 提供了一个特化版本,用于交换两个相同大小的数组的内容。这个特化版本等效于调用 std::swap_ranges 来逐个元素地交换数组中的元素。

template <class T2, std::size_t N>
void swap(T2 (&a)[N], T2 (&b)[N]);

这个版本的 std::swap 要求 T2 是可交换的(即 std::is_swappable_v<T2>true),并且它的复杂度是线性于数组的大小 N

4. std::swap 的重载解析

在 C++17 之前,std::swap 的重载解析依赖于 T 是否满足可移动构造和可移动赋值的要求。从 C++17 开始,引入了 std::is_swappable_v<T> 概念,用于更精确地控制 std::swap 的可用性。只有当 std::is_swappable_v<T>true 时,std::swap 才会参与重载解析。

5. 自定义类型的 std::swap 实现

对于自定义类型,标准库允许你通过以下两种方式提供 std::swap 的特化或重载:

5.1 全局命名空间中的 swap 函数

最推荐的方式是在与自定义类型相同的命名空间中定义一个非成员函数 swap。这样可以通过 ADL(Argument-Dependent Lookup,基于参数的查找)自动找到该函数,而不需要显式地调用 std::swap。例如:

namespace MyNamespace {
    class MyClass {
        int value;
    public:
        MyClass(int v) : value(v) {}
        // 其他成员函数...
    };

    // 在同一个命名空间中定义 swap 函数
    void swap(MyClass& a, MyClass& b) noexcept {
        using std::swap;
        swap(a.value, b.value);  // 使用 std::swap 交换成员变量
    }
}

在这个例子中,swap 函数会在 MyNamespace 中定义,并且可以通过 ADL 自动找到。如果你有一个 MyClass 对象并调用 std::swap,编译器会优先选择 MyNamespace::swap,因为它是通过 ADL 找到的。

5.2 std::swap 的特化

虽然可以在 namespace std 中特化 std::swap,但这是不推荐的做法,除非你有非常明确的理由。特化 std::swap 会导致代码依赖于标准库的内部实现,并且可能会引发未定义行为。更好的做法是按照上述方式在自定义类型的命名空间中定义 swap 函数。

6. 标准库提供的 std::swap 特化

C++ 标准库为许多常用类型提供了 std::swap 的特化版本,以优化交换操作。以下是一些常见的特化:

  • 容器类:如 std::vectorstd::liststd::mapstd::set 等容器类都有自己的 std::swap 特化,通常比通用的 std::swap 更高效。
  • 智能指针:如 std::shared_ptrstd::unique_ptr 也有特化的 std::swap,可以避免不必要的资源复制。
  • 字符串类:如 std::stringstd::wstring 等字符串类也有特化的 std::swap,能够高效地交换字符串内容。
  • 其他标准库类型:如 std::pairstd::tuplestd::optionalstd::variant 等也都有特化的 std::swap

7. 示例代码

下面是一个完整的示例,展示了如何使用 std::swap 以及如何为自定义类型定义 swap 函数:

#include <algorithm>
#include <iostream>
#include <utility>

namespace Ns {
    class A {
        int id;

        friend void swap(A& lhs, A& rhs) noexcept {
            std::cout << "swap(" << lhs << ", " << rhs << ")\n";
            std::swap(lhs.id, rhs.id);
        }

        friend std::ostream& operator<<(std::ostream& os, const A& a) {
            return os << "A::id=" << a.id;
        }

    public:
        A(int i) : id(i) {}
        A(const A&) = delete;  // 禁用拷贝构造
        A& operator=(const A&) = delete;  // 禁用拷贝赋值
    };
}

int main() {
    // 交换基本类型的值
    int a = 5, b = 3;
    std::cout << "Before swap: " << a << ' ' << b << '\n';
    std::swap(a, b);
    std::cout << "After swap: " << a << ' ' << b << '\n';

    // 交换自定义类型的值
    Ns::A p(6), q(9);
    std::cout << "Before swap: " << p << ' ' << q << '\n';
    swap(p, q);  // 使用 ADL 找到 Ns::swap
    std::cout << "After swap: " << p << ' ' << q << '\n';

    // 交换数组
    int arr1[] = {1, 2, 3};
    int arr2[] = {4, 5, 6};
    std::cout << "Before swap: ";
    for (int i : arr1) std::cout << i << ' ';
    std::cout << " | ";
    for (int i : arr2) std::cout << i << ' ';
    std::cout << '\n';
    std::swap(arr1, arr2);
    std::cout << "After swap: ";
    for (int i : arr1) std::cout << i << ' ';
    std::cout << " | ";
    for (int i : arr2) std::cout << i << ' ';
    std::cout << '\n';
}

8. 输出结果

Before swap: 5 3
After swap: 3 5
Before swap: A::id=6 A::id=9
swap(A::id=6, A::id=9)
After swap: A::id=9 A::id=6
Before swap: 1 2 3  | 4 5 6 
After swap: 4 5 6  | 1 2 3 

9. 注意事项

  • ADL(基于参数的查找):当你调用 std::swap 时,编译器会首先在当前作用域中查找 swap 函数,然后在参数所属的命名空间中查找。因此,如果你在自定义类型的命名空间中定义了 swap 函数,编译器会优先使用该函数,而不是 std::swap

  • 移动语义std::swap 的实现依赖于移动构造和移动赋值操作。确保你的类型支持这些操作,或者为你的类型提供适当的 swap 函数。

  • 性能优化:对于某些复杂类型(如容器),标准库提供的 std::swap 特化版本通常比通用的 std::swap 更高效。因此,尽量使用标准库提供的特化版本,而不是自己实现。

  • noexceptconstexpr:从 C++11 和 C++20 开始,std::swap 支持 noexceptconstexpr,这有助于编写更安全和高效的代码。确保你在定义自定义类型的 swap 函数时也考虑这些特性。

10. 总结

std::swap 是 C++ 标准库中用于交换两个对象值的重要工具。它不仅适用于基本类型,还适用于复杂的自定义类型和标准库类型。通过为自定义类型提供适当的 swap 函数,你可以确保交换操作的效率和安全性。标准库为许多常用类型提供了优化的 std::swap 特化版本,你应该尽量使用这些特化版本,以获得最佳性能。

如果你有更多具体的问题或需要进一步的帮助,请随时提问!

std::exchange

std::exchange 是 C++ 标准库中的一个实用函数,最早在 C++14 中引入。它用于将对象的当前值替换为新值,并返回对象的旧值。这个函数非常有用,特别是在实现移动语义、资源管理类以及需要原子性地更新和获取旧值的场景中。

1. std::exchange 的基本用法

std::exchange 的定义如下:

template <class T, class U = T>
constexpr T exchange(T& obj, U&& new_value) noexcept(/* conditions */);
  • obj:要替换其值的对象。
  • new_value:要分配给 obj 的新值。
  • 返回值obj 的旧值。
类型要求:
  • T 必须满足 MoveConstructible 的要求。
  • U 必须能够被移动赋值给 T
异常安全性:
  • 从 C++23 开始,std::exchange 被标记为 noexcept,具体取决于 T 的移动构造和赋值操作是否是 noexcept 的。
noexcept(
    std::is_nothrow_move_constructible_v<T> &&
    std::is_nothrow_assignable_v<T&, U>
)

2. std::exchange 的可能实现

std::exchange 的实现通常如下所示:

template <class T, class U = T>
constexpr T exchange(T& obj, U&& new_value)
    noexcept(
        std::is_nothrow_move_constructible_v<T> &&
        std::is_nothrow_assignable_v<T&, U>
    )
{
    T old_value = std::move(obj);  // 移动构造旧值
    obj = std::forward<U>(new_value);  // 移动赋值新值
    return old_value;  // 返回旧值
}

3. std::exchange 的应用场景

std::exchange 在以下几个场景中特别有用:

3.1 实现移动赋值运算符和移动构造函数

std::exchange 可以简化移动赋值运算符和移动构造函数的实现。通过使用 std::exchange,你可以在一次操作中同时获取旧值并将其替换为新值,而不需要手动保存旧值和进行赋值。

例如,考虑以下类 S,它包含一个整数成员 n

struct S {
    int n;

    // 移动构造函数
    S(S&& other) noexcept : n{std::exchange(other.n, 0)} {}

    // 移动赋值运算符
    S& operator=(S&& other) noexcept {
        n = std::exchange(other.n, 0);  // 将 other.n 移动到 this->n,并将 0 留在 other.n 中
        return *this;
    }
};

在这个例子中,std::exchange 确保了 other.n 的旧值被移动到 this->n,同时将 0 留在 other.n 中。这种方式不仅简洁,而且避免了潜在的自我赋值问题。

3.2 在类中实现属性的 getter 和 setter

std::exchange 可以用于在类中实现属性的 getter 和 setter,特别是当你希望在设置新值的同时返回旧值时。例如:

class stream {
public:
    using flags_type = int;

    // 获取当前的标志
    flags_type flags() const { return flags_; }

    // 设置新的标志,并返回旧的标志
    flags_type flags(flags_type newf) { return std::exchange(flags_, newf); }

private:
    flags_type flags_ = 0;
};

在这个例子中,flags 方法不仅设置了新的标志,还返回了旧的标志,这在某些情况下非常有用(例如,用户可能希望知道旧的设置)。

3.3 在循环中使用 std::exchange

std::exchange 也可以用于循环中,特别是在你需要在每次迭代中更新变量并使用旧值的情况下。例如,生成斐波那契数列时:

std::cout << "Fibonacci sequence: ";
for (int a{0}, b{1}; a < 100; a = std::exchange(b, a + b)) {
    std::cout << a << ", ";
}
std::cout << "...\n";

在这个例子中,std::exchange(b, a + b) 同时计算了 b 的新值 a + b,并将旧的 b 值赋给 a,从而实现了斐波那契数列的生成。

3.4 处理函数指针或可调用对象

std::exchange 还可以用于处理函数指针或可调用对象。例如,你可以使用 std::exchange 来替换一个函数指针,并立即调用旧的函数:

void f() { std::cout << "f()"; }

int main() {
    void (*fun)();

    // 替换 fun,并立即调用旧的函数(如果有的话)
    std::exchange(fun, f)();
    
    return 0;
}

在这个例子中,std::exchange(fun, f)fun 替换为 f,并返回旧的 fun,然后立即调用旧的 fun

4. std::exchange 的示例代码

下面是一个完整的示例,展示了 std::exchange 的多种用法:

#include <iostream>
#include <iterator>
#include <utility>
#include <vector>

// 定义一个流类,带有标志属性
class stream {
public:
    using flags_type = int;

    // 获取当前的标志
    flags_type flags() const { return flags_; }

    // 设置新的标志,并返回旧的标志
    flags_type flags(flags_type newf) { return std::exchange(flags_, newf); }

private:
    flags_type flags_ = 0;
};

// 定义一个函数
void f() { std::cout << "f()"; }

int main() {
    // 测试流类的 flags 方法
    stream s;
    std::cout << "Initial flags: " << s.flags() << '\n';
    std::cout << "Old flags after setting to 12: " << s.flags(12) << '\n';
    std::cout << "Current flags: " << s.flags() << "\n\n";

    // 使用 std::exchange 替换 vector 的内容
    std::vector<int> v;
    std::exchange(v, {1, 2, 3, 4});
    std::copy(begin(v), end(v), std::ostream_iterator<int>(std::cout, ", "));
    std::cout << "\n\n";

    // 使用 std::exchange 替换函数指针
    void (*fun)();
    std::exchange(fun, f)();
    std::cout << "\n\n";

    // 使用 std::exchange 生成斐波那契数列
    std::cout << "Fibonacci sequence: ";
    for (int a{0}, b{1}; a < 100; a = std::exchange(b, a + b)) {
        std::cout << a << ", ";
    }
    std::cout << "...\n";

    return 0;
}

5. 输出结果

Initial flags: 0
Old flags after setting to 12: 0
Current flags: 12

1, 2, 3, 4,

f()

Fibonacci sequence: 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, ...

6. 注意事项

  • 移动语义std::exchange 的实现依赖于移动构造和移动赋值操作。确保你的类型支持这些操作,或者为你的类型提供适当的 swap 函数。

  • noexceptconstexpr:从 C++20 和 C++23 开始,std::exchange 支持 constexprnoexcept,这有助于编写更安全和高效的代码。确保你在定义自定义类型的 exchange 函数时也考虑这些特性。

  • 模板参数推导std::exchange 的第二个模板参数 U 有一个默认值 T,这意味着你可以传递任何可以隐式转换为 T 的类型作为新值。这使得 std::exchange 非常灵活,适用于各种场景。

7. 总结

std::exchange 是 C++ 标准库中的一个强大工具,特别适用于需要原子性地更新和获取旧值的场景。它简化了移动语义的实现,减少了代码冗余,并提高了代码的可读性和安全性。通过合理使用 std::exchange,你可以编写更加简洁、高效和健壮的代码。

如果你有更多具体的问题或需要进一步的帮助,请随时提问!

std::ranges::swap

std::ranges::swap 是 C++20 引入的一个新工具,属于 C++20 范围库(Ranges Library)的一部分。它提供了一种更加通用和灵活的方式来交换两个对象的值,特别是在处理范围(如数组、容器等)时。与传统的 std::swap 不同,std::ranges::swap 旨在更好地支持范围操作,并且能够根据上下文选择最合适的交换方法。

1. std::ranges::swap 的定义和调用签名

std::ranges::swap 定义在头文件 <concepts> 中,并且是一个自定义点对象(customization point object)。它的调用签名如下:

namespace std::ranges {
    inline constexpr /* unspecified */ swap = /* unspecified */;
}

实际的调用签名是:

template <class T, class U>
constexpr void ranges::swap(T&& t, U&& u) noexcept(/* see below */);

2. std::ranges::swap 的行为

std::ranges::swap 的行为取决于传递给它的参数类型。它会根据以下规则依次尝试不同的交换方法:

2.1 类或枚举类型的交换

如果 tu 是类或枚举类型,并且存在一个有效的 swap 函数可以通过 ADL(Argument-Dependent Lookup)找到,则 std::ranges::swap 会调用该函数:

(void)swap(t, u);

这里的 swap 函数可以在命名空间 std::ranges 中查找,并且有一个附加的候选函数模板 template<class T> void swap(T&, T&) = delete;,以防止不适当的匹配。

2.2 数组范围的交换

如果 tu 是相同大小的左值数组(但可能具有不同的元素类型),并且 ranges::swap(*t, *u) 是有效的表达式,则 std::ranges::swap 会调用 ranges::swap_ranges 来逐个元素地交换数组中的值:

(void)ranges::swap_ranges(t, u);

此时,noexcept 规范为:

noexcept((void)ranges::swap_ranges(t, u)) == noexcept(ranges::swap(*t, *u))
2.3 普通左值的交换

如果 tu 都是相同类型 V 的左值,并且 V 满足 std::move_constructible<V>std::assignable_from<V&, V> 的要求,则 std::ranges::swap 会使用标准的移动构造和赋值操作来交换它们的值:

{
    V v1(std::move(t));
    V v2(std::move(u));
    t = std::move(v2);
    u = std::move(v1);
}

此时,noexcept 规范为:

noexcept(std::is_nothrow_move_constructible_v<V> && std::is_nothrow_move_assignable_v<V>)
2.4 常量表达式的交换

如果上述交换操作是一个常量表达式,则 V 必须是一个 LiteralType,并且 t = std::move(u)u = std::move(t) 都必须是常量子表达式。此外,V v1(std::move(t));V v2(std::move(u)); 的完整表达式也必须是常量子表达式。

2.5 格式错误的情况

如果上述所有条件都不满足,则 std::ranges::swap 格式错误,这会导致替换失败,特别是在模板实例化的直接上下文中。

3. std::ranges::swap 的自定义点对象特性

std::ranges::swap 是一个自定义点对象,这意味着它是一个常量函数对象,属于字面量 semiregular 类类型。所有的 __swap_fn 实例都是相等的,并且可以自由复制。调用不同类型 __swap_fn 实例对相同参数的影响是等效的,无论表示该实例的表达式是左值还是右值,并且是否有 const 限定。

4. std::ranges::swap 的优势

std::ranges::swap 相比于传统的 std::swap 有以下几个优势:

  • 更好的范围支持std::ranges::swap 可以更智能地处理范围(如数组、容器等),并根据具体情况选择最合适的交换方法。
  • 更灵活的类型支持std::ranges::swap 支持异构交换(即不同类型的对象之间的交换),例如在示例中展示了 IntLikeint 之间的交换。
  • 更好的异常安全性std::ranges::swapnoexcept 规范更加精确,确保了在可能的情况下提供更强的异常安全性保证。

5. 示例代码

下面是一个完整的示例,展示了 std::ranges::swap 的多种用法:

#include <array>
#include <concepts>
#include <iostream>
#include <ranges>
#include <string_view>
#include <vector>

// 打印两个范围的内容
void print(std::string_view name, 
           std::ranges::common_range auto const& p, 
           std::ranges::common_range auto const& q)
{
    std::cout << name << "1{ ";
    for (auto const& i : p)
        std::cout << i << ' ';
    std::cout << "}, " << name << "2{ ";
    for (auto const& i : q)
        std::cout << i << ' ';
    std::cout << "}\n";
}

// 打印两个整数
void print(std::string_view name, int p, int q)
{
    std::cout << name << "1 = " << p << ", " << name << "2 = " << q << '\n';
}

// 自定义类型 IntLike
struct IntLike {
    int v;
};

// 为 IntLike 和 int 提供异构交换
void swap(IntLike& lhs, int& rhs) {
    std::swap(lhs.v, rhs);
}

void swap(int& lhs, IntLike& rhs) {
    std::swap(lhs, rhs.v);
}

std::ostream& operator<<(std::ostream& out, IntLike i) {
    return out << i.v;
}

int main() {
    // 交换 vector
    std::vector a1{10, 11, 12}, a2{13, 14};
    std::ranges::swap(a1, a2);
    print("a", a1, a2);

    // 交换 array
    std::array b1{15, 16, 17}, b2{18, 19, 20};
    std::ranges::swap(b1, b2);
    print("b", b1, b2);

    // 交换数组(相同大小)
    int d1[]{21, 22, 23}, d2[]{24, 25, 26};
    std::ranges::swap(d1, d2);
    print("d", d1, d2);

    // 交换 IntLike 和 int 数组(异构交换)
    IntLike g1[]{1, 2, 3};
    int     g2[]{4, 5, 6};
    std::ranges::swap(g1, g2);
    print("g", g1, g2);

    // 交换两个整数
    int h1{27}, h2{28};
    std::ranges::swap(h1, h2);
    print("h", h1, h2);

    return 0;
}

6. 输出结果

a1{ 13 14 }, a2{ 10 11 12 }
b1{ 18 19 20 }, b2{ 15 16 17 }
d1{ 24 25 26 }, d2{ 21 22 23 }
g1{ 4 5 6 }, g2{ 1 2 3 }
h1 = 28, h2 = 27

7. 注意事项

  • 范围大小必须匹配:当交换两个数组时,它们的大小必须相同,否则会导致编译错误。例如,std::ranges::swap(c1, c2) 会因为 c1c2 的大小不同而失败。

  • 元素类型的兼容性:当交换两个数组时,虽然它们的元素类型可以不同,但必须能够通过 ADL 找到合适的 swap 函数来交换元素。例如,在示例中,IntLikeint 之间的交换是通过自定义的 swap 函数实现的。

  • 异常安全性std::ranges::swapnoexcept 规范确保了在可能的情况下提供更强的异常安全性保证。特别是当交换操作涉及到移动构造和赋值时,noexcept 规范会检查这些操作是否是 noexcept 的。

  • 常量表达式支持:从 C++20 开始,std::ranges::swap 支持 constexpr,这意味着它可以用于常量表达式中,只要交换操作本身是常量表达式。

8. 总结

std::ranges::swap 是 C++20 中引入的一个强大工具,特别适用于处理范围(如数组、容器等)的交换操作。它不仅提供了更好的灵活性和类型支持,还确保了更好的异常安全性和常量表达式支持。通过合理使用 std::ranges::swap,你可以编写更加简洁、高效和健壮的代码。

如果你有更多具体的问题或需要进一步的帮助,请随时提问!

std::forward

std::forward 是 C++11 引入的一个非常重要的工具,用于实现完美转发(perfect forwarding)。完美转发允许我们将参数以原始的值类别(左值或右值)传递给另一个函数,而不会丢失其原始特性。这对于编写通用代码(如模板函数和类)至关重要,特别是在处理移动语义和构造函数时。

1. std::forward 的定义和用法

std::forward 定义在头文件 <utility> 中,有以下两个重载:

template <class T>
constexpr T&& forward(std::remove_reference_t<T>& t) noexcept;

template <class T>
constexpr T&& forward(std::remove_reference_t<T>&& t) noexcept;
1.1 第一个重载
template <class T>
constexpr T&& forward(std::remove_reference_t<T>& t) noexcept;
  • 用途:将左值转发为左值或右值,具体取决于 T
  • 行为
    • 如果 t 是一个转发引用(即 T&& 是对模板参数的右值引用),则 std::forward 会根据 T 的类型决定是将其作为左值还是右值转发。
    • 如果 T 被推断为左值引用(如 T&const T&),则 std::forwardt 作为左值转发。
    • 如果 T 被推断为非引用类型(如 Tconst T),则 std::forwardt 作为右值转发。
1.2 第二个重载
template <class T>
constexpr T&& forward(std::remove_reference_t<T>&& t) noexcept;
  • 用途:将右值转发为右值,并禁止将右值转发为左值。
  • 行为:此重载确保 t 始终作为右值转发。如果尝试将右值转发为左值(例如通过使用左值引用类型 T 实例化此重载),会导致编译错误。

2. std::forward 的工作原理

std::forward 的核心在于它能够保留传递给它的表达式的值类别(左值或右值)。这在模板编程中尤为重要,因为模板参数的类型推导规则可能会改变表达式的值类别。

例如,考虑以下模板函数:

template <typename T>
void wrapper(T&& arg) {
    foo(std::forward<T>(arg));
}

在这个例子中,wrapper 函数接受一个转发引用 T&&,这意味着 arg 可以是左值或右值。std::forward<T>(arg) 确保 arg 以原始的值类别传递给 foo

  • 如果 wrapper 被调用时传递的是一个右值(如临时对象),则 T 被推断为非引用类型(如 int),std::forwardarg 作为右值转发。
  • 如果 wrapper 被调用时传递的是一个左值(如命名变量),则 T 被推断为左值引用(如 int&),std::forwardarg 作为左值转发。

3. std::forward 的应用场景

std::forward 主要用于以下场景:

3.1 完美转发构造函数参数

std::forward 经常用于构造函数模板中,以确保参数以原始的值类别传递给成员对象的构造函数。例如:

struct A {
    template <typename T1, typename T2, typename T3>
    A(T1&& t1, T2&& t2, T3&& t3)
        : a1_{std::forward<T1>(t1)},
          a2_{std::forward<T2>(t2)},
          a3_{std::forward<T3>(t3)} {}
    
private:
    std::string a1_;
    int a2_;
    double a3_;
};

在这个例子中,std::forward 确保 t1t2t3 以原始的值类别传递给 a1_a2_a3_ 的构造函数。

3.2 工厂函数中的完美转发

std::forward 也常用于工厂函数中,以确保参数以原始的值类别传递给构造函数。例如:

template <typename T, typename... Args>
std::unique_ptr<T> make_unique(Args&&... args) {
    return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
}

在这个例子中,std::forward<Args>(args)... 确保 args 以原始的值类别传递给 T 的构造函数。

3.3 成员函数调用中的完美转发

std::forward 还可以用于成员函数调用中,以确保成员函数的结果以原始的值类别传递。例如:

struct Arg {
    int i = 1;
    int get() && { return i; }  // 右值引用重载
    int& get() &  { return i; } // 左值引用重载
};

template <typename T>
void wrapper(T&& arg) {
    foo(std::forward<decltype(std::forward<T>(arg).get())>(std::forward<T>(arg).get()));
}

在这个例子中,std::forward<T>(arg).get() 确保 get() 的结果以原始的值类别传递给 foo

4. std::forward_like (C++23)

从 C++23 开始,引入了 std::forward_like,它提供了一种更灵活的方式来转发远对象(far objects)。std::forward_like 的主要目的是适应“远”对象,即那些需要通过多个层次的间接访问的对象(如嵌套的智能指针、可选对象等)。

std::forward_like 的签名如下:

template <class T, class U>
constexpr auto&& forward_like(U&& x) noexcept;
4.1 std::forward_like 的行为

std::forward_like 返回对 x 的引用,该引用具有与 T&& 类似的属性。具体来说:

  • 如果 std::remove_reference_t<T> 是一个 const 修饰的类型,则返回类型的引用类型是 const std::remove_reference_t<U>
  • 否则,引用类型是 std::remove_reference_t<U>
  • 如果 T&& 是一个左值引用类型,则返回类型也是一个左值引用类型。
  • 否则,返回类型是一个右值引用类型。
4.2 std::forward_like 的应用场景

std::forward_like 主要用于处理远对象的转发问题。例如:

struct FarStates {
    std::unique_ptr<TypeTeller> ptr;
    std::optional<TypeTeller> opt;
    std::vector<TypeTeller> container;

    auto&& from_opt(this auto&& self) {
        return std::forward_like<decltype(self)>(self.opt.value());
    }

    auto&& operator[](this auto&& self, std::size_t i) {
        return std::forward_like<decltype(self)>(self.container.at(i));
    }

    auto&& from_ptr(this auto&& self) {
        if (!self.ptr)
            throw std::bad_optional_access{};
        return std::forward_like<decltype(self)>(*self.ptr);
    }
};

在这个例子中,std::forward_like 确保 self.opt.value()self.container.at(i)*self.ptr 的结果以正确的值类别和 const 修饰符传递。

5. 示例代码

以下是一个完整的示例,展示了 std::forwardstd::forward_like 的用法:

#include <cstddef>
#include <iostream>
#include <memory>
#include <optional>
#include <type_traits>
#include <utility>
#include <vector>

struct TypeTeller {
    void operator()(this auto&& self) {
        using SelfType = decltype(self);
        using UnrefSelfType = std::remove_reference_t<SelfType>;
        if constexpr (std::is_lvalue_reference_v<SelfType>) {
            if constexpr (std::is_const_v<UnrefSelfType>)
                std::cout << "const lvalue\n";
            else
                std::cout << "mutable lvalue\n";
        } else {
            if constexpr (std::is_const_v<UnrefSelfType>)
                std::cout << "const rvalue\n";
            else
                std::cout << "mutable rvalue\n";
        }
    }
};

struct FarStates {
    std::unique_ptr<TypeTeller> ptr;
    std::optional<TypeTeller> opt;
    std::vector<TypeTeller> container;

    auto&& from_opt(this auto&& self) {
        return std::forward_like<decltype(self)>(self.opt.value());
    }

    auto&& operator[](this auto&& self, std::size_t i) {
        return std::forward_like<decltype(self)>(self.container.at(i));
    }

    auto&& from_ptr(this auto&& self) {
        if (!self.ptr)
            throw std::bad_optional_access{};
        return std::forward_like<decltype(self)>(*self.ptr);
    }
};

int main() {
    FarStates my_state{
        .ptr{std::make_unique<TypeTeller>()},
        .opt{std::in_place, TypeTeller{}},
        .container{std::vector<TypeTeller>(1)},
    };

    my_state.from_ptr()();  // mutable lvalue
    my_state.from_opt()();  // mutable lvalue
    my_state[0]();         // mutable lvalue

    std::cout << '\n';

    std::as_const(my_state).from_ptr()();  // const lvalue
    std::as_const(my_state).from_opt()();  // const lvalue
    std::as_const(my_state)[0]();          // const lvalue

    std::cout << '\n';

    std::move(my_state).from_ptr()();      // mutable rvalue
    std::move(my_state).from_opt()();      // mutable rvalue
    std::move(my_state)[0]();              // mutable rvalue

    std::cout << '\n';

    std::move(std::as_const(my_state)).from_ptr()();  // const rvalue
    std::move(std::as_const(my_state)).from_opt()();  // const rvalue
    std::move(std::as_const(my_state))[0]();          // const rvalue
}

6. 输出结果

mutable lvalue
mutable lvalue
mutable lvalue

const lvalue
const lvalue
const lvalue

mutable rvalue
mutable rvalue
mutable rvalue

const rvalue
const rvalue
const rvalue

7. 总结

std::forward 是 C++ 模板编程中不可或缺的工具,用于实现完美转发。它确保参数以原始的值类别传递给目标函数,从而避免不必要的拷贝和移动操作。std::forward_like(C++23)进一步扩展了这一功能,特别适用于远对象的转发问题,确保了更复杂的嵌套结构也能正确地保留值类别和 const 修饰符。

如果你有更多具体的问题或需要进一步的帮助,请随时提问!

std::move

std::move 是 C++11 引入的一个重要工具,用于指示一个对象可以被“移动”(即资源可以从该对象有效地转移到另一个对象)。它通过将左值转换为右值表达式来实现这一点,从而允许调用移动语义(如移动构造函数和移动赋值运算符),而不是复制语义。

1. std::move 的定义和用法

std::move 定义在头文件 <utility> 中,其签名如下:

template <class T>
constexpr std::remove_reference_t<T>&& move(T&& t) noexcept;
1.1 行为

std::move 的主要作用是将一个左值表达式转换为右值表达式。具体来说,std::move(t) 等价于 static_cast<std::remove_reference_t<T>&&>(t),即将 t 转换为 T&& 类型的右值引用。

  • 左值:命名变量(如 str)是左值。
  • 右值:临时对象(如函数返回值)是右值。

通过 std::move,我们可以将左值转换为右值,从而允许移动语义的使用。

1.2 为什么需要 std::move

C++ 的重载解析机制会根据参数的值类别(左值或右值)选择不同的函数重载。例如,类的移动构造函数和移动赋值运算符通常接受右值引用参数。如果我们直接传递一个左值给这些函数,编译器会选择复制构造函数或复制赋值运算符,而不是移动构造函数或移动赋值运算符。

std::move 的作用就是将左值转换为右值,以便调用移动构造函数或移动赋值运算符,从而避免不必要的拷贝操作,提高性能。

2. std::move 的应用场景

2.1 移动构造函数

移动构造函数用于从一个对象“窃取”资源,而不是复制资源。例如:

class A {
public:
    // Move constructor
    A(A&& other) noexcept : data(other.data) {
        other.data = nullptr; // 窃取资源并使 other 处于有效但未指定状态
    }

private:
    int* data;
};

在这个例子中,A(A&& other) 是移动构造函数,它接受一个右值引用参数 other。我们可以通过 std::move 将左值转换为右值,从而调用移动构造函数:

A a1("Hello");
A a2(std::move(a1)); // 调用移动构造函数
2.2 移动赋值运算符

移动赋值运算符用于从另一个对象“窃取”资源,并将当前对象的内容释放。例如:

class A {
public:
    // Move assignment operator
    A& operator=(A&& other) noexcept {
        if (this != &other) {
            delete data; // 释放当前对象的资源
            data = other.data; // 窃取 other 的资源
            other.data = nullptr; // 使 other 处于有效但未指定状态
        }
        return *this;
    }

private:
    int* data;
};

在这个例子中,operator=(A&& other) 是移动赋值运算符,它接受一个右值引用参数 other。我们可以通过 std::move 将左值转换为右值,从而调用移动赋值运算符:

A a1("Hello");
A a2;
a2 = std::move(a1); // 调用移动赋值运算符
2.3 标准库中的移动语义

许多标准库容器和算法也支持移动语义。例如,std::vectorpush_back 成员函数有两个重载版本:一个接受左值引用,另一个接受右值引用。通过 std::move,我们可以将左值转换为右值,从而调用移动版本的 push_back,避免不必要的拷贝操作。

std::string str = "Hello";
std::vector<std::string> v;

v.push_back(str); // 调用 push_back(const T&),进行拷贝
v.push_back(std::move(str)); // 调用 push_back(T&&),进行移动

3. std::move 的注意事项

3.1 移动后对象的状态

使用 std::move 后,原对象处于“有效但未指定状态”。这意味着对象仍然满足其类的不变量,但其内容可能是未定义的。因此,虽然可以继续对对象进行某些操作(如赋值、析构等),但不能依赖其具体值。

例如:

std::string str = "Hello";
std::vector<std::string> v;
v.push_back(std::move(str));

// str 现在处于有效但未指定状态
if (!str.empty()) { // OK,empty() 没有先决条件
    std::cout << str.back(); // OK,如果 str 不为空
}

str.clear(); // OK,clear() 没有先决条件
3.2 自我移动赋值

C++ 标准库保证,标准库类型的自我移动赋值不会导致未定义行为。也就是说,即使我们将一个对象移动赋值给自己,对象仍然会被置于有效状态。

std::vector<int> v = {1, 2, 3};
v = std::move(v); // v 现在处于有效但未指定状态
3.3 转发引用与 std::forward

当函数参数是转发引用(即模板参数的右值引用)时,不应使用 std::move,而应使用 std::forward 来保留参数的原始值类别。例如:

template <typename T>
void wrapper(T&& arg) {
    foo(std::forward<T>(arg)); // 保留 arg 的原始值类别
}

4. 示例代码

以下是一个完整的示例,展示了 std::move 的用法:

#include <iomanip>
#include <iostream>
#include <string>
#include <utility>
#include <vector>

int main() {
    std::string str = "Salut";
    std::vector<std::string> v;

    // 使用复制构造函数
    v.push_back(str);
    std::cout << "After copy, str is " << std::quoted(str) << '\n';

    // 使用移动构造函数
    v.push_back(std::move(str));
    std::cout << "After move, str is " << std::quoted(str) << '\n';

    std::cout << "The contents of the vector are {" << std::quoted(v[0])
              << ", " << std::quoted(v[1]) << "}\n";

    // 检查移动后的字符串是否为空
    if (!str.empty()) {
        std::cout << "str is not empty: " << std::quoted(str) << '\n';
    } else {
        std::cout << "str is empty\n";
    }

    // 清空字符串
    str.clear();
    std::cout << "After clear, str is " << std::quoted(str) << '\n';

    return 0;
}

5. 输出结果

After copy, str is "Salut"
After move, str is ""
The contents of the vector are {"Salut", "Salut"}
str is empty
After clear, str is ""

6. 总结

std::move 是 C++11 引入的一个重要工具,用于指示对象可以被移动,而不是复制。它通过将左值转换为右值表达式,使得编译器可以选择移动构造函数或移动赋值运算符,从而提高性能。使用 std::move 后,原对象处于“有效但未指定状态”,因此不能依赖其具体值。此外,标准库类型保证自我移动赋值不会导致未定义行为。

如果你有更多具体的问题或需要进一步的帮助,请随时提问!

std::move_if_noexcept 是 C++11 引入的一个实用工具,用于在移动操作可能抛出异常的情况下,决定是使用移动语义还是复制语义。它通过检查类型的移动构造函数是否为 noexcept(即不会抛出异常)来做出决策。如果移动构造函数是 noexcept 的,或者类型没有拷贝构造函数(只能移动),则 std::move_if_noexcept 会返回一个右值引用,允许使用移动语义;否则,它会返回一个左值引用,强制使用复制语义。

1. std::move_if_noexcept 的定义和用法

std::move_if_noexcept 定义在头文件 <utility> 中,其签名如下:

template <class T>
constexpr std::conditional_t<
    std::is_nothrow_move_constructible_v<T> || !std::is_copy_constructible_v<T>,
    T&&,
    const T&
> move_if_noexcept(T& x) noexcept;
1.1 行为
  • 如果 T 的移动构造函数是 noexcept 的,或者 T 没有拷贝构造函数(只能移动),则 std::move_if_noexcept(x) 返回 T&&,即右值引用。
  • 否则,std::move_if_noexcept(x) 返回 const T&,即左值引用。
1.2 为什么需要 std::move_if_noexcept

std::move_if_noexcept 的主要目的是确保在移动操作可能抛出异常的情况下,程序仍然能够提供强异常保证(strong exception guarantee)。强异常保证意味着,如果操作失败,程序的状态不会发生任何变化。

例如,在 std::vector::resize 中,可能会需要分配新的存储空间并将元素从旧的存储空间移动或复制到新的存储空间。如果在此过程中发生异常,std::vector::resize 需要撤消所有已经完成的操作,以保持容器的一致性。为了实现这一点,std::vector::resize 使用 std::move_if_noexcept 来决定是使用移动构造函数还是复制构造函数。只有当移动构造函数是 noexcept 时,才会使用移动语义,否则会使用复制语义以确保强异常保证。

2. std::move_if_noexcept 的应用场景

2.1 标准库中的使用

std::move_if_noexcept 在标准库中广泛使用,尤其是在涉及资源管理的类(如 std::vectorstd::deque 等)中。这些类在进行内存分配和元素转移时,通常会使用 std::move_if_noexcept 来决定是使用移动语义还是复制语义,以确保在异常情况下程序状态的一致性。

例如,std::vector::resize 可能会使用 std::move_if_noexcept 来决定如何将元素从旧的存储空间转移到新的存储空间:

void resize(size_type new_size, const value_type& value) {
    // 分配新存储空间
    allocate_new_storage(new_size);

    try {
        for (size_type i = 0; i < new_size; ++i) {
            if (i < old_size) {
                // 使用 move_if_noexcept 决定是移动还是复制
                new_storage[i] = std::move_if_noexcept(old_storage[i]);
            } else {
                new_storage[i] = value;
            }
        }
    } catch (...) {
        // 如果发生异常,撤消所有已完成的操作
        deallocate_new_storage();
        throw;
    }

    // 释放旧存储空间
    deallocate_old_storage();
}
2.2 用户自定义类型的使用

std::move_if_noexcept 也可以在用户自定义类型中使用,特别是在编写支持移动语义的类时。例如,假设我们有一个类 ResourceHolder,它持有一些资源,并且我们希望在移动操作可能抛出异常的情况下提供强异常保证:

class ResourceHolder {
public:
    ResourceHolder() : resource(nullptr) {}
    ResourceHolder(ResourceHolder&& other) noexcept(false) : resource(other.resource) {
        other.resource = nullptr;
        // 模拟可能抛出异常的操作
        if (some_condition) {
            throw std::runtime_error("Move operation failed");
        }
    }
    ResourceHolder(const ResourceHolder&) = delete;
    ~ResourceHolder() { release_resource(); }

    void swap(ResourceHolder& other) noexcept {
        std::swap(resource, other.resource);
    }

private:
    void* resource;
    void release_resource() {
        if (resource != nullptr) {
            // 释放资源
            resource = nullptr;
        }
    }
};

// 使用 std::move_if_noexcept 进行安全的移动
void safe_transfer(ResourceHolder& from, ResourceHolder& to) {
    try {
        to = std::move_if_noexcept(from);
    } catch (...) {
        // 如果移动失败,恢复 from 的状态
        from.release_resource();
        throw;
    }
}

在这个例子中,safe_transfer 函数使用 std::move_if_noexcept 来决定是移动 from 的资源还是复制 from 的资源。如果移动操作可能抛出异常,则会使用复制语义,以确保在异常情况下 from 的状态不会被破坏。

3. 示例代码

以下是一个完整的示例,展示了 std::move_if_noexcept 的用法:

#include <iostream>
#include <utility>

struct Bad {
    Bad() {}
    Bad(Bad&&) { std::cout << "Throwing move constructor called\n"; }
    Bad(const Bad&) { std::cout << "Throwing copy constructor called\n"; }
};

struct Good {
    Good() {}
    Good(Good&&) noexcept { std::cout << "Non-throwing move constructor called\n"; }
    Good(const Good&) noexcept { std::cout << "Non-throwing copy constructor called\n"; }
};

int main() {
    Good g;
    Bad b;

    // 对于 Good 类型,使用移动构造函数
    [[maybe_unused]] Good g2 = std::move_if_noexcept(g);

    // 对于 Bad 类型,使用复制构造函数
    [[maybe_unused]] Bad b2 = std::move_if_noexcept(b);

    return 0;
}

4. 输出结果

Non-throwing move constructor called
Throwing copy constructor called

5. 总结

std::move_if_noexcept 是一个非常有用的工具,特别适用于需要提供强异常保证的场景。它通过检查类型的移动构造函数是否为 noexcept 来决定是使用移动语义还是复制语义。这使得程序可以在移动操作可能抛出异常的情况下,仍然保持一致性和安全性。

如果你有更多具体的问题或需要进一步的帮助,请随时提问!

std::to_underlying 是 C++23 引入的一个实用工具函数,用于将枚举类型的值转换为其底层类型。它提供了一种安全且明确的方式,将枚举值转换为整数类型,而不需要显式地使用 static_cast 或其他类型的强制转换。

1. std::to_underlying 的定义和用法

std::to_underlying 定义在头文件 <utility> 中,其签名如下:

template <class Enum>
constexpr std::underlying_type_t<Enum> to_underlying(Enum e) noexcept;
1.1 行为

std::to_underlying(e) 等价于 static_cast<std::underlying_type_t<Enum>>(e),即将枚举值 e 转换为其底层类型的整数值。std::underlying_type_t<Enum> 是一个类型别名,表示枚举 Enum 的底层类型。

1.2 为什么需要 std::to_underlying

在 C++ 中,枚举类型的值可以隐式转换为整数类型,但这可能会导致意外的行为,尤其是在不同平台或编译器之间,枚举的底层类型可能不同。std::to_underlying 提供了一种明确且类型安全的方式来获取枚举值的底层整数值,避免了潜在的不兼容性问题。

此外,std::to_underlying 还可以帮助编写更清晰的代码,因为它明确表达了你希望将枚举值转换为其底层类型,而不是随意将其转换为任意整数类型。

2. std::to_underlying 的应用场景

2.1 枚举与整数之间的转换

当你需要将枚举值作为整数传递给某些函数或进行数学运算时,std::to_underlying 可以确保你得到的是正确的底层整数值。例如:

enum class Color {
    red = 0xFF,
    green = 0x00FF00,
    blue = 0x0000FF
};

void print_color_value(std::uint32_t value) {
    std::cout << "Color value: " << std::hex << value << std::dec << '\n';
}

int main() {
    Color color = Color::red;
    print_color_value(std::to_underlying(color)); // 安全地将枚举值转换为 uint32_t
}
2.2 避免隐式转换错误

在某些情况下,直接将枚举值赋值给整数类型可能会导致编译错误或意外行为。std::to_underlying 可以帮助你避免这些问题。例如:

enum class ColorMask : std::uint32_t {
    red = 0xFF, green = (0xFF << 8), blue = (0xFF << 16), alpha = (0xFF << 24)
};

int main() {
    // 错误:不能直接将枚举值赋值给底层类型
    // std::uint32_t x = ColorMask::alpha;

    // 正确:使用 std::to_underlying 进行安全转换
    std::uint32_t y = std::to_underlying(ColorMask::alpha);
}
2.3 静态断言

std::to_underlying 也可以与 static_assert 一起使用,确保枚举的底层类型符合预期。例如:

#include <type_traits>
#include <utility>

enum class E1 : char { e };
static_assert(std::is_same_v<char, decltype(std::to_underlying(E1::e))>);

enum struct E2 : long { e };
static_assert(std::is_same_v<long, decltype(std::to_underlying(E2::e))>);

enum E3 : unsigned { e };
static_assert(std::is_same_v<unsigned, decltype(std::to_underlying(E3::e))>);

3. 示例代码

以下是一个完整的示例,展示了 std::to_underlying 的用法:

#include <cstdint>
#include <iomanip>
#include <iostream>
#include <type_traits>
#include <utility>

enum class ColorMask : std::uint32_t {
    red = 0xFF, green = (red << 8), blue = (green << 8), alpha = (blue << 8)
};

int main() {
    std::cout << std::hex << std::uppercase << std::setfill('0')
              << std::setw(8) << std::to_underlying(ColorMask::red) << '\n'
              << std::setw(8) << std::to_underlying(ColorMask::green) << '\n'
              << std::setw(8) << std::to_underlying(ColorMask::blue) << '\n'
              << std::setw(8) << std::to_underlying(ColorMask::alpha) << '\n';

    // 错误:不能直接将枚举值赋值给底层类型
    // std::underlying_type_t<ColorMask> x = ColorMask::alpha;

    // 正确:使用 std::to_underlying 进行安全转换
    [[maybe_unused]]
    std::underlying_type_t<ColorMask> y = std::to_underlying(ColorMask::alpha);

    return 0;
}

4. 输出结果

000000FF
0000FF00
00FF0000
FF000000

5. 总结

std::to_underlying 是 C++23 引入的一个非常有用的工具,用于将枚举类型的值安全地转换为其底层类型。它提供了一种明确且类型安全的方式,避免了隐式转换带来的潜在问题,并且可以使代码更加清晰和易读。

如果你有更多具体的问题或需要进一步的帮助,请随时提问!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

丁金金_chihiro_修行

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值