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&&
)在模板中是不够的。因为:
如果参数是左值,我们需要接收它并处理为左值。
如果参数是右值,我们希望保留右值特性,以便在转发时触发移动操作,而非拷。
使用
T&&
与std::forward
实现完美转发使用
T&&
和std::forward
能让模板函数接收左值或右值并保持其原始类型,这种方式称为“万能引用”或“转发引用”。结合std::forward
,可以在不确定传入参数是左值还是右值的情况下,实现完美转发:
传入左值时,
T
推导为T&
,T&&
会折叠为T&
,即左值引用。传入右值时,
T
推导为T
,T&&
保持为右值引用,完美保留右值属性。
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
}