C++ - 类型操作

forward

std::forward 是一个函数模板,通常与右值引用(rvalue references)结合使用。它的主要作用是实现完美转发(perfect forwarding),确保在函数模板中能够保留传入参数的值类别(左值或右值)。这个特性在泛型编程和模板代码中非常重要,可以避免不必要的拷贝或移动,从而提高效率。它的作用有点像“快递转发”——当我们在模板函数里接收到一个参数时,不直接使用它,而是通过 std::forward 转发出去,这样能保证参数在转发的过程中不发生额外的拷贝或移动。比如,如果传进来的是一个“右值”(临时变量),那用 std::forward 转发时会保持它的“右值”身份,这样可以避免不必要的性能开销。

使用例子

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

void tellMeType(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 << "常量左值\n";
        else
            std::cout << "可变左值\n";
    }
    else
    {
        if constexpr (std::is_const_v<UnrefSelfType>)
            std::cout << "常量右值\n";
        else
            std::cout << "可变右值\n";
    }
}

void foo(std::string& s) {
    std::cout << "Left Value: " << s << "\n";
}

void foo(std::string&& s) {
    std::cout << "Right Value: " << s << "\n";
}

template<typename T>
void handle(T&& args) {
    foo(std::forward<T>(args));
    tellMeType(args);
    tellMeType(std::forward<T>(args));
}

int main() {
    std::string s = "aaaa";
    handle(s);
    handle(std::string {"bbbb"});
    auto& s1 = s;
    handle(s1);
    return 0;
}

输出结果:

Left Value: aaaa
可变左值
可变左值
Right Value: bbbb
可变左值
可变右值
Left Value: aaaa
可变左值
可变左值

错误用例

void process(int& x);    // 处理左值
void process(int&& x);   // 处理右值

template <typename T>
void forward_test(T&& arg) {
    process(arg); // 如果不使用 std::forward,右值被当作左值传递
}

forward_test(10); // 期望调用右值版本,但会调用左值版本

有人会说:“我自己实现一个方法入参就是引用类型不就是了。”

直接用引用类型确实可以避免拷贝,因为引用可以直接绑定到原始对象上而不需要创建副本。但是,当涉及到泛型代码时,只用普通引用类型无法应对左值和右值的区别,这就是 std::forward 和右值引用(T&&)的关键所在。

对于泛型编程(如模板函数)来说,我们并不确定传进来的参数是左值还是右值。C++中,左值和右值通常代表不同的语义

  • 左值表示一个可以重复使用的对象。

  • 右值表示一个临时对象,通常希望能“移动”而不是“拷贝”它,以减少资源开销。

直接用左值引用(T&)或右值引用(T&&)在模板中是不够的。因为:

  1. 如果参数是左值,我们需要接收它并处理为左值。

  2. 如果参数是右值,我们希望保留右值特性,以便在转发时触发移动操作,而非拷。

使用 T&&std::forward 实现完美转发

使用 T&&std::forward 能让模板函数接收左值或右值并保持其原始类型,这种方式称为“万能引用”或“转发引用”。结合 std::forward,可以在不确定传入参数是左值还是右值的情况下,实现完美转发:

  1. 传入左值时,T 推导为 T&T&& 会折叠为 T&,即左值引用。

  2. 传入右值时,T 推导为 TT&& 保持为右值引用,完美保留右值属性。

forward_like

std::forward_like 是 C++23 引入的一个新工具,进一步扩展了 std::forward 的能力。它允许我们“模拟”另一个对象的引用和类别(左值或右值)来进行转发。

#include <utility>
#include <print>
#include <iostream>

void process(int& x) { 
    x = 100; // 左值则修改原来的值
    std::cout << "Processing lvalue int" << std::endl;
}

void process(int&& x) {
    // 右值无视
    std::cout << "Processing rvalue int" << std::endl;
}

template <typename Pair>
void handle_pair(Pair&& p) {
    process(std::forward_like<Pair>(p.first));
    process(std::forward_like<Pair>(p.second));
}

template <typename Pair>
void handle_pair_old(Pair&& p) { // 旧版转发
    process(std::forward<int>(p.first));
    process(std::forward<int>(p.second));
}

int main() {
    int a = 10;
    auto p = std::pair<int&, int&>{a, a};
    handle_pair_old(std::pair<int&, int&&>{a, 20});
    std::cout << a << "\n";
    handle_pair_old(p);
    std::cout << a << "\n";

    std::cout << "================ New " << "\n";

    handle_pair(p);
    std::cout << a << "\n";

    a = 10;
    handle_pair(std::pair<int&, int&&>{a, 20});
    std::cout << a << "\n";
}

上方的使用场景中,我在使用原始人forward的时候,总是被转发到右值处理器,而我pair中first/second给他的是一个左值,即使我给一个左值的pair也无济于事,这个时候forward_like却可以正常工作,提供了更强大的表达能力。

当然你也可以在某些复杂场景中,如果没有 std::forward_like,通过模板推断和多个重载来手动判断并区分左值、右值,但是这不仅增加了代码复杂度,还容易导致错误。std::forward_like 通过“模仿”来自动处理这些细节,简化了代码编写。

输出结果:

Processing rvalue int
Processing rvalue int
10
Processing rvalue int
Processing rvalue int
10
================ New 
Processing lvalue int
Processing lvalue int
100
Processing rvalue int
Processing rvalue int
10

move

移动,不完全是移动,美名其曰的移动。目的在:转换实参为亡值!

返回值:static_cast<typename std::remove_reference<T>::type&&>(t)

使用move之后将触发亡语移动构造函数 or 移动赋值运算符

比如说std::string把这两个函数都进行了重载,其中会将原始的string标记为死亡(不是真正意义的move,这两个操作都建立在新的string已分配内存空间的情况下,并不会减少内存分配次数,当然现代计算机也不缺这几次拷贝了)

#include <iostream>
#include <utility>
#include <cstring>

class MyClass {
private:
    int* data;
    size_t size;

public:
    // 构造函数
    MyClass(size_t size) : size(size), data(new int[size]) {
        std::cout << "Constructing MyClass with size " << size << std::endl;
        std::memset(data, 0, size * sizeof(int));  // 初始化为0
    }

    // 移动构造函数
    MyClass(MyClass&& other) noexcept 
        : data(other.data), size(other.size) {
        std::cout << "Move constructing MyClass" << std::endl;
        
        // 将 other 置于“亡语”状态
        other.data = nullptr;
        other.size = 0;
    }

    // 移动赋值运算符
    MyClass& operator=(MyClass&& other) noexcept {
        std::cout << "Move assigning MyClass" << std::endl;
        
        if (this != &other) {
            // 释放当前对象持有的资源
            delete[] data;

            // 转移资源
            data = other.data;
            size = other.size;

            // 将 other 置于“亡语”状态
            other.data = nullptr;
            other.size = 0;
        }
        return *this;
    }

    // 析构函数
    ~MyClass() {
        delete[] data;
        std::cout << "Destroying MyClass" << std::endl;
    }

    // 打印数据
    void print() const {
        if (data) {
            for (size_t i = 0; i < size; ++i) {
                std::cout << data[i] << " ";
            }
            std::cout << std::endl;
        } else {
            std::cout << "Object is in moved-from state" << std::endl;
        }
    }

    // 判断对象是否处于有效状态
    bool is_valid() const {
        return data != nullptr;
    }
};

int main() {
    MyClass obj1(5);         // 创建对象
    obj1.print();            // 打印数据

    MyClass obj2 = std::move(obj1);  // 移动构造
    obj2.print();            // 打印 obj2 数据
    obj1.print();            // 打印 obj1 数据,处于亡语状态

    MyClass obj3(10);
    obj3 = std::move(obj2);  // 移动赋值
    obj3.print();            // 打印 obj3 数据
    obj2.print();            // 打印 obj2 数据,处于亡语状态

    return 0;
}

上方代码揭示了move使用的全流程。

move_if_noexcept

这玩意比较幽默,若实参的移动构造函数不抛异常,则 move_if_noexcept 获得到实参的右值引用,否则获得左值引用。它典型地用于组合移动语义和强异常保证。

#include <iostream>
#include <utility>
 
struct Bad
{
    Bad() {}
    Bad(Bad&&) // 可能抛出
    {
        std::cout << "调用了可能抛出的移动构造函数\n";
    }
    Bad(const Bad&) // 亦可能抛出
    {
        std::cout << "调用了可能抛出的复制构造函数\n";
    }
};
 
struct Good
{
    Good() {}
    Good(Good&&) noexcept // 将不抛出
    {
        std::cout << "调用了无抛出的移动构造函数\n";
    }
    Good(const Good&) noexcept // 将不抛出
    {
        std::cout << "调用了无抛出的复制构造函数\n";
    }
};
 
int main()
{
    Good g;
    Bad b;
    [[maybe_unused]] Good g2 = std::move_if_noexcept(g);
    [[maybe_unused]] Bad b2 = std::move_if_noexcept(b);
}

as_const

#include <cassert>
#include <string>
#include <type_traits>
#include <utility>
 
int main()
{
    std::string mutableString = "Hello World!";
    auto&& constRef = std::as_const(mutableString);
 
//  mutableString.clear(); // OK
//  constRef.clear(); // 错误:'constRef' 有 'const' 限定,但 'clear' 不标记为 const
 
    assert(&constRef == &mutableString);
    assert(&std::as_const(mutableString) == &mutableString);
 
    using ExprType = std::remove_reference_t<decltype(std::as_const(mutableString))>;
 
    static_assert(std::is_same_v<std::remove_const_t<ExprType>, std::string>,
                  "ExprType 应当为某种字符串。");
    static_assert(!std::is_same_v<ExprType, std::string>,
                  "ExprType 不能是可修改的字符串。");
}

declval

#include <iostream>
#include <utility>
 
struct Default
{
    int foo() const { return 1; }
};
 
struct NonDefault
{
    NonDefault() = delete;
    int foo() const { return 1; }
};
 
int main()
{
    decltype(Default().foo()) n1 = 1;                   // n1 的类型是 int
//  decltype(NonDefault().foo()) n2 = n1;               // 错误:无默认构造函数
    decltype(std::declval<NonDefault>().foo()) n2 = n1; // n2 的类型是 int
    std::cout << "n1 = " << n1 << '\n'
              << "n2 = " << n2 << '\n';
}

to_underlying

转换枚举到其底层类型。等价于 return static_cast<std::underlying_type_t<Enum>>(e);

#include <cstdint>
#include <iomanip>
#include <iostream>
#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(e))>);
 
int main()
{
    enum class ColorMask : std::uint32_t
    {
        red = 0xFF, green = (red << 8), blue = (green << 8), alpha = (blue << 8)
    };
 
    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; // 错误:无已知转换
    [[maybe_unused]]
    std::underlying_type_t<ColorMask> y = std::to_underlying(ColorMask::alpha); // OK
}