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- 限定符。那么当然也可以用模板。

cpp

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 objectExplicit Object Parameter
void foo() &;void foo(this X&);
void foo() const &;void foo(this const X&);
void foo() &&;void foo(this X&&);

在调用一个这样的成员函数的时候,这个对象会作为第一个参数,其他参数会依次推后一个。 ( 比如括号里的第一个参数,实际是声明里的第二个参数。 )

考虑下面两个不同的函数声明,他们的类型分别为:

cpp

struct Y {
    int f(int, int) const&;
    int g(this Y const&, int, int);
};
  • &Y::f is int(Y::*)(int, int) const&
  • &Y::g is int(*)(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 argumentYesYesNo
隐式声明了 thisYesNoNo
可以声明重载运算符YesYesNo
&C::f 的类型成员函数指针函数指针函数指针
虚函数?YesMaybeNo
auto f = x.f;NoNoYes
名字可以退化成函数指针NoNoYes

有了 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 的函数会被视作静态函数,并且在参与重载决议的时候, 参数被视作从第二个参数开始。所以下面这两个函数定义是冲突的:

cpp

static void foo();
void foo(this X const&);

3 Use cases

类似 std::optional<T> 的例子:

cpp

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();
    }
};

cpp

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();
    }
};

lambda 一直都有个问题是递归比较困难,因为没法拿到 lambda 函数自己这个对象。 所以一般都是用些 trick 的方法。要不就是转成一个 std::function 的对象存起来, 要不就是要在每次调用的时候,都把自己作为一个参数传给自己,像这样:

cpp

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);

cpp

auto fib = [](this auto self, int i) {
    if (i < 2) return 1;
    return self(i - 1) + self(i - 2);
};
Update 2023.11.8

这个递归甚至可以被用在 std::visit 里面:

cpp

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);
}

CRTP 一个重要的部分,就是要在基类里得到子类的类型,这个一般都是把基类变成一个模板类, 然后在继承的时候把子类类型传进去得到的。有 Deducing this 之后,explicit object member function 的 explicit object parameter 是模板的话,就可以在调用的时候得到实际子类的类型。

cpp

template<typename D>
struct CRTP {
    void foo() {
        static_cast<D*>(this)->fooImpl();
    }
};

cpp

struct CRTP {
    void foo(this auto& self) {
        self.fooImpl();
    }
};

cpp

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; }
};

对于像 std::string_view 这类的小对象,传值是个比传引用更快,所以应该更多传值而不是传引用, 对于这类的对象。但是在以前一个函数调用会把 *this 作为一个参数传着。对于这种对象, 把 *this 变成 Self 显然是个很大的性能优化。

cpp

struct StringView {
    auto length(this Stringview self) { return self.length; }
};

4 References:

  1. C++ Draft: Duducing this

Related Content