C++ std::forward 的使用
C++真实一门细节比较多的语言,稍不注意就会出现奇怪请琢磨不透的bug,这时候就说明你的C++基础不扎实。
C++ lvalue rvalue
std::string hello = "Hello World";
顾名思义左值就是等号左边的hello
, 右值就是等号右边的字符串Hello World
;
观察一下左值指向了右值,左值指向的右值是可以替换的。左值代表一个具体的存储位置,右值代表一个临时的值.
左值右值的重载函数
class Person {
private:std::string name;
public:// 构造函数Person(const std::string& name) : name(name) {std::cout << "Person constructed: " << name << std::endl;}// 析构函数~Person() {std::cout << "Person destructed: " << name << std::endl;}// 拷贝构造函数Person(const Person& other) : name(other.name) {std::cout << "Person copy-constructed: " << name << std::endl;}// 移动构造函数Person(Person&& other) noexcept : name(std::move(other.name)) {std::cout << "Person move-constructed: " << name << std::endl;}// 赋值运算符Person& operator=(const Person& other) {if (this != &other) {name = other.name;std::cout << "Person copy-assigned: " << name << std::endl;}return *this;}// 移动赋值运算符Person& operator=(Person&& other) noexcept {if (this != &other) {name = std::move(other.name);std::cout << "Person move-assigned: " << name << std::endl;}return *this;}const std::string& getName() const { return name; }void setName(const std::string& newName) { name = newName; }friend std::ostream& operator<<(std::ostream& os, const Person& person) {os << "Person(Name: " << person.name << ")";return os;}
};void consume(Person& p){std::cout << "consume(Person&): " << p << " (lvalue overload)\n";
}void consume(const Person& p){std::cout << "consume(const Person&): " << p << " (lvalue overload)\n";
}void consume(Person&& p) {std::cout << "consume(Person&&): " << p << " (rvalue overload)\n";
}void consume(const Person&& p) {std::cout << "consume(const Person&&): " << p << " (rvalue overload)\n";
}
存在普通重载函数是不能存在左值、右值重载函数的。即:
void consume(Person p){std::cout << "consume(Person): " << p << " (lvalue overload)\n";
}
触发重载函数:
Person alice("Alice");
consume(alice);
// consume(std::string("Alice")); // 1
代码1处使用到了隐式转换, 如果需要禁止使用explicit
修饰构造函数. 当然隐式转换只能一次。比如const char*
-> std::string
-> Person
就不支持。
可以尝试删除上面四个重载函数,什么时候继续会执行,什么时候会编译报错。
函数模板
最简单的函数模板
template<typename T>
T add(T a, T b){return a + b;
}
使用C++20 Concepts
加一些限制
template<typename T>
requires requires(T a, T b) { { a + b } -> std::convertible_to<T>; }
T add(T a, T b){return a + b;
}struct Point {int x;int y;friend std::ostream& operator<<(std::ostream& os, const Point& point){os << "Point(" << point.x << ", " << point.y << ")";return os;}Point operator+(const Point& other) const {return Point{x + other.x, y + other.y};}
};void learn001() {int a = 1;int b = 2;Point p1{1, 2};Point p2{3, 4};std::cout << add(a, b) << "\n";std::cout << add(p1, p2) << "\n";
}
当然也可以使用C++11/C++14 的 SFINAE
,当然其较为复杂。
TODO: 出一篇简单解释
C++20 Concepts
和C++11/C++14 的 SFINAE
的博客
requires requires(T a, T b) { { a + b } -> std::convertible_to<T>;
}
{ a + b } -> std::convertible_to<T>;
是一个lambda.
a + b
: 检查是否可以进行相加操作;
std::convertible_to<T>
: 表示相加的结果是否可以隐式转换为T
std::forward<T>
的作用
std::forward<T>
的源码:
template <typename T>
T&& forward(typename std::remove_reference<T>::type& t) noexcept {return static_cast<T&&>(t);
}
class Person {
private:std::string name;
public:// 构造函数Person(const std::string& name) : name(name) {std::cout << "Person constructed: " << name << std::endl;}// 析构函数~Person() {std::cout << "Person destructed: " << name << std::endl;}// 拷贝构造函数Person(const Person& other) : name(other.name) {std::cout << "Person copy-constructed: " << name << std::endl;}// 移动构造函数Person(Person&& other) noexcept : name(std::move(other.name)) {std::cout << "Person move-constructed: " << name << std::endl;}// 赋值运算符Person& operator=(const Person& other) {if (this != &other) {name = other.name;std::cout << "Person copy-assigned: " << name << std::endl;}return *this;}// 移动赋值运算符Person& operator=(Person&& other) noexcept {if (this != &other) {name = std::move(other.name);std::cout << "Person move-assigned: " << name << std::endl;}return *this;}const std::string& getName() const { return name; }void setName(const std::string& newName) { name = newName; }friend std::ostream& operator<<(std::ostream& os, const Person& person) {os << "Person(Name: " << person.name << ")";return os;}
};void consume(Person& p){std::cout << "consume(Person&): " << p << " (lvalue overload)\n";
}void consume(Person&& p) {std::cout << "consume(Person&&): " << p << " (rvalue overload)\n";
}template<typename T>
void wrapper_forward(T&& arg) { // arg is a universal reference (万能引用) 虽然类型是右值类型,但是arg是一个左值std::cout << "wrapper_forward(T&&): called\n";// consume(std::forward<T>(arg)); // 1// consume(std::forward<T&>(arg)); // error// consume(std::forward<T&&>(arg)); // errorconsume(arg);
}template<typename T>
void wrapper_forward(T& arg) {std::cout << "wrapper_forward(T&): called\n";// consume(arg);consume(std::forward<T>(arg)); // 2// consume(std::forward<T&>(arg)); // error// consume(std::forward<T&&>(arg)); // error
}void learn001() {Person alice("alice");wrapper_forward(alice); // 3
}
试图触发wrapper_forward
重载函数, 并试着注释或者打开代码1、2,观察结果。
我解释一下执行过程:
传入左值alice
,调用重载函数wrapper_forward(T&)
, T
被推断为Person
,所以代码2处
consume(std::forward<T>(arg));
为consume(std::forward<Person>(arg));
而arg
类型是Person&
。
带入std::forward<T>
源码如下:
Person&& forward(Person& t) noexcept {return static_cast<Person&&>(t);
}
折叠规律(万能引用):
折叠 | 结果 |
---|---|
T& && | T& |
举个例子:
void consume(Person& p){std::cout << "consume(Person&): " << p << " (lvalue overload)\n";
}void consume(Person&& p) {std::cout << "consume(Person&&): " << p << " (rvalue overload)\n";
}template<typename T>
void wrapper_forward(T&& arg) { // arg is a universal reference (万能引用) 虽然类型是右值类型,但是arg是一个左值std::cout << "wrapper_forward(T&&): called\n";consume(std::forward<T>(arg));// consume(std::forward<T&>(arg)); // error// consume(std::forward<T&&>(arg)); // error// consume(arg);
}template <typename T>
T&& forward(typename std::remove_reference<T>::type& t) noexcept {return static_cast<T&&>(t);
}void learn001() {Person alice("alice");wrapper_forward(alice); // 1wrapper_forward(std::move(alice)); // 2
}
T
被推断为Person&
,所以wrapper_forward(Person& &&)
,根据折叠规律结果为Person&
, 之后 强转为Person&
;T
被推断为Person&&
,所以wrapper_forward(Person&& &&)
,根据折叠规律结果为Person&&
, 之后 强转为Person&&
;
代码验证
template<typename T>
void wrapper_forward(T&& arg) { std::cout << "wrapper_forward(T&): called\n";std::cout << "T is Person? " << std::is_same_v<T, Person> << "\n";std::cout << "T is Person&? " << std::is_same_v<T, Person&> << "\n";std::cout << "T is Person&&? " << std::is_same_v<T, Person&&> << "\n";std::cout << "arg type is Person? " << std::is_same_v<decltype(arg), Person> << "\n";std::cout << "arg type is Person&? " << std::is_same_v<decltype(arg), Person&> << "\n";std::cout << "arg type is Person&&? " << std::is_same_v<decltype(arg), Person&&> << "\n";std::cout << "param type: " << (std::is_lvalue_reference_v<decltype(arg)> ? "lvalue ref" : "rvalue ref") << "\n";consume(std::forward<T>(arg));// consume(std::forward<T&>(arg)); // error// consume(std::forward<T&&>(arg)); // error// consume(arg);
}template <typename T>
T&& forward(typename std::remove_reference<T>::type& t) noexcept {return static_cast<T&&>(t);
}void learn001() {Person alice("alice");wrapper_forward(std::move(alice));
}