指向(数据)成员的指针作为非类型模板参数,例如具有自动存储持续时间/无链接

Pointer to (data) member as non-type template parameter for instance with automatic storage duration / without linkage

考虑以下片段:

#include <cstdint>
#include <iostream>

struct Foo {
    Foo() : foo_(0U), bar_(0U) {}

    void increaseFoo() { increaseCounter<&Foo::foo_>(); }
    void increaseBar() { increaseCounter<&Foo::bar_>(); }

    template <uint8_t Foo::*counter>
    void increaseCounter() { ++(this->*counter); }

    uint8_t foo_;
    uint8_t bar_;
};

void callMeWhenever() {
    Foo f;  // automatic storage duration, no linkage.
    f.increaseFoo();
    f.increaseFoo();
    f.increaseBar();

    std::cout << +f.foo_ << " " << +f.bar_;  // 2 1
}

int main() {
    callMeWhenever();
}

我的第一个猜测是它的格式不正确,因为 callMeWhenever() 中的 f 具有自动存储持续时间,并且其地址在编译时未知,而成员模板Foo 的函数 increaseCounter() 使用指向 Foo 的数据成员的指针实例化,给定 class 类型的内存表示是特定于编译器的(例如填充)。但是,根据 cppreference / Template parameters and template arguments,afaics,这是合式的:

Template non-type arguments

The following limitations apply when instantiating templates that have non-type template parameters:

[..]

[until C++17] For pointers to members, the argument has to be a pointer to member expressed as &Class::Member or a constant expression that evaluates to null pointer or std::nullptr_tvalue.

[..]

[since C++17] The only exceptions are that non-type template parameters of reference or pointer type [added since C++20: and non-static data members of reference or pointer type in a non-type template parameter of class type and its subobjects (since C++20)] cannot refer to/be the address of

  • a subobject (including non-static class member, base subobject, or array element);
  • a temporary object (including one created during reference initialization);
  • a string literal;
  • the result of typeid;
  • or the predefined variable __func__.

这是如何运作的?编译器(通过直接或间接,例如上述标准要求)是否需要自行解决这个问题,仅存储成员之间的(编译时)地址偏移量,而不是实际地址?

即/例如,是Foo::increaseCounter()中指向数据成员非类型模板参数counter的编译时指针(对于指向数据成员实例的两个特定指针中的每一个)只是一个编译Foo 的任何给定实例化的时间地址偏移量,稍后将成为 Foo 的每个实例的完全解析地址,即使对于尚未分配的 f 块范围内的实例也是如此callMeWhenever()?

Is the compiler (by direct or indirect, e.g. the above, standard requirements) required to sort this out by itself, storing only (compile-time) address offsets between the members, rather than actual addresses?

差不多。即使在编译时上下文之外,它也是一个 "offset"。指向成员的指针与常规指针不同。他们指定成员,而不是对象。这也意味着关于有效 pointer 目标的废话与指向成员的指针无关。

这就是为什么要从它们中产生一个实际的左值,必须用指向一个对象的东西来完成图片,就像我们在 this->*counter 中所做的那样。如果您尝试在需要常量表达式的地方使用 this->*counter,编译器会抱怨,但它应该是 this,而不是 counter.

它们独特的性质使它们可以无条件地成为编译时常量。没有编译器必须检查为有效目标的对象。

正如 StoryTeller 已经提到的,指向成员的指针与普通指针不同。如果我们看一下 clang 生成的(几乎)未优化的程序集(完整代码 here),我们会看到模板的实例化:

void Foo::increaseCounter<&Foo::foo_>(): # @void Foo::increaseCounter<&Foo::foo_>()
        add     byte ptr [rdi], 1 
        ret

void Foo::increaseCounter<&Foo::bar_>(): # @void Foo::increaseCounter<&Foo::bar_>()
        add     byte ptr [rdi + 1], 1
        ret

因为这些是成员函数 rdi(这是第一个函数参数)持有指向 class 实例(在我们的例子中是 this)的指针。因为 Foo::foo_ 是第一个成员,它的地址与其 class 相匹配,所以 &f== &f.foo_ (其中 fFoo 的一个实例)。因此,对于 Foo::foo_,我们只需获取 this 的地址并将该地址指向的字节递增 1。

第二种情况类似。唯一的区别是 Foo::bar_ 是 class 中的第二个数据成员,因为 Foo::foo_ 只占用 space 的 1 个字节,所以 Foo::bar_ 位于 reinterpret_cast<char*>(this) + 1rdi + 1.

反映