Deducing This
1 What is deducing this?
A new way of declaring non-static member functions that will allow for deducing the type and value category of the class instance parameter while still being invocable with regular member function syntax.
简单的说,就是让非静态成员函数的 this
参数成为一个显式参数。而不破坏以前的调用成员函数的语法。
2 Semantics
The new syntax is to use an explicit
this
-annotated parameter.
一个非静态成员函数可以把它的第一个参数成为一个显式对象参数
(explicit object parameter),用一个关键字前缀 this
来表示。
这个参数可以像其他参数一样,使用 cv- 和 ref- 限定符。那么当然也可以用模板。
struct Foo {
void foo(this const Foo&);
void foo(this volatile Foo&&);
template<typename Self>
void bar(this Self self);
void barbar(this auto self);
};
非静态成员函数,隐式决定对象类型和显式对象参数类型的表示:
Implicit object | Explicit Object Parameter |
---|---|
void foo() &; | void foo(this X&); |
void foo() const &; | void foo(this const X&); |
void foo() &&; | void foo(this X&&); |
在调用一个这样的成员函数的时候,这个对象会作为第一个参数,其他参数会依次推后一个。 ( 比如括号里的第一个参数,实际是声明里的第二个参数。 )
2.1 Function Type
考虑下面两个不同的函数声明,他们的类型分别为:
struct Y {
int f(int, int) const&;
int g(this Y const&, int, int);
};
&Y::f
isint(Y::*)(int, int) const&
&Y::g
isint(*)(const Y&, int, int)
也就是说,带有 explicit object parameter 的函数可以理解成被视作静态成员函数的。 所以静态成员函数也不能有 explicit object parameter。
实际上带有 explicit object parameter 不同与非静态成员函数和静态成员函数, 是另一种成员函数类型。
They’re like semi-static member functions. We’ll call them explicit object member functions due to them having an explicit object parameter.
非静态 | EOMF | 静态 | |
---|---|---|---|
Requires/uses an object argument | Yes | Yes | No |
隐式声明了 this | Yes | No | No |
可以声明重载运算符 | Yes | Yes | No |
&C::f 的类型 | 成员函数指针 | 函数指针 | 函数指针 |
虚函数? | Yes | Maybe | No |
auto f = x.f; | No | No | Yes |
名字可以退化成函数指针 | No | No | Yes |
2.2 Candidate Functions
有了 Deducing this
之后,新加入了带有 explicit object parameter 的 candidates。
The only change in how we look up candidate functions is in the case of an explicit object parameter, where the argument list is shifted by one. The first listed parameter is bound to the object argument, and the second listed parameter corresponds to the first argument of the call expression.
由于带 explicit object parameter 的函数会被视作静态函数,并且在参与重载决议的时候, 参数被视作从第二个参数开始。所以下面这两个函数定义是冲突的:
static void foo();
void foo(this X const&);
3 Use cases
3.1 避免重复
类似 std::optional<T>
的例子:
3.1.1 Before
template<typename T>
struct Optional {
auto value() const & {
if (has_value()) {
return value_;
}
throw std::bad_optional_access();
}
auto value() const&& {
if (has_value()) {
return std::move(value_);
}
throw std::bad_optional_access();
}
auto value() && {
if (has_value()) {
return std::move(value_);
}
throw std::bad_optional_access();
}
auto value() & {
if (has_value()) {
return value_;
}
throw std::bad_optional_access();
}
};
3.1.2 After deducing this
template<typename T>
struct Optional {
auto value(this auto&& self) {
if (self.has_value()) {
return std::forward_like<decltype(self)>(self.value_);
}
throw std::bad_optional_access();
}
};
3.2 Recursive Lambdas
lambda 一直都有个问题是递归比较困难,因为没法拿到 lambda 函数自己这个对象。
所以一般都是用些 trick 的方法。要不就是转成一个 std::function
的对象存起来,
要不就是要在每次调用的时候,都把自己作为一个参数传给自己,像这样:
3.2.1 Before
std::function<int(int)> fib;
fib = [&fib](int i) {
if (i < 2) return 1;
return fib(i - 1) + fib(i - 2);
};
auto fib2 = [](auto& fib, int i) {
if (i < 2) return 1;
return fib(i - 1) + fib(i - 2);
};
std::cout << fib2(fib2, 10);
3.2.2 After deducing this
auto fib = [](this auto self, int i) {
if (i < 2) return 1;
return self(i - 1) + self(i - 2);
};
这个递归甚至可以被用在 std::visit
里面:
struct Leaf {};
struct Node;
using Tree = std::variant<Leaf, Node*>;
struct Node {
Tree left, right;
};
template <typename... Ts>
struct overload : Ts... { using Ts::operator()...; }
int countLeaves(const Tree& tree) {
return std::visit(overload{
[] (const Leaf&) { return 1; },
[] (this const auto& self, const Node* node) -> int {
return visit(self, node->left) + visit(self, node->right);
}
},
tree);
}
3.3 CRTP without CRT
CRTP
一个重要的部分,就是要在基类里得到子类的类型,这个一般都是把基类变成一个模板类,
然后在继承的时候把子类类型传进去得到的。有 Deducing this
之后,explicit object
member function 的 explicit object parameter 是模板的话,就可以在调用的时候得到实际子类的类型。
3.3.1 Before
template<typename D>
struct CRTP {
void foo() {
static_cast<D*>(this)->fooImpl();
}
};
3.3.2 After deducing this
struct CRTP {
void foo(this auto& self) {
self.fooImpl();
}
};
3.3.3 By-value member functions: Move-into-parameter chaining
struct Builder {
Builder name(this Builder self, auto value) { self.name_ = std::move(value); return self; }
Builder password(this Builder self, auto value) { self.password_ = std::move(value); return self; }
};
3.3.4 By-value member function: Performance improvement
对于像 std::string_view
这类的小对象,传值是个比传引用更快,所以应该更多传值而不是传引用,
对于这类的对象。但是在以前一个函数调用会把 *this
作为一个参数传着。对于这种对象,
把 *this
变成 Self
显然是个很大的性能优化。
struct StringView {
auto length(this Stringview self) { return self.length; }
};