【问题标题】:Pointer to (data) member as non-type template parameter for instance with automatic storage duration / without linkage指向(数据)成员的指针作为非类型模板参数,例如具有自动存储持续时间/无链接
【发布时间】:2020-01-09 13:43:11
【问题描述】:

考虑以下 sn-p:

#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 具有自动存储持续时间,并且它的地址在编译时是未知的,而@ 的成员模板函数increaseCounter() 987654327@ 使用指向Foo 数据成员的指针进行实​​例化,并且给定类类型的内存表示是编译器特定的(例如填充)。但是,来自cppreference / Template parameters and template arguments,afaics,这是格式正确的:

模板非类型参数

在实例化具有非类型模板参数的模板时适用以下限制:

[..]

[直到 C++17] 对于指向成员的指针,参数必须是指向成员的指针,表示为 &amp;Class::Member 或计算结果为空指针或 std::nullptr_tvalue 的常量表达式.

[..]

[自 C++17] 唯一的例外是引用或指针类型的非类型模板参数 [自 C++20: 和非静态数据成员添加类类型及其子对象的非类型模板参数中的引用或指针类型(C++20 起)] 不能引用/作为地址

  • 子对象(包括非静态类成员、基子对象或数组元素);
  • 一个临时对象(包括在引用初始化期间创建的一个);
  • 字符串文字;
  • typeid 的结果;
  • 或预定义变量__func__。

这是如何工作的?编译器(通过直接或间接,例如上述标准要求)是否需要自行对其进行排序,仅存储成员之间的(编译时)地址偏移量,而不是实际地址?

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

【问题讨论】:

  • 引用的“since C++17”部分看起来很可疑。有关信息,最新的draft of the Standard 允许指针类型的非类型模板参数是非静态类成员的地址。
  • @YSC 谢谢;我从引用中省略了 C++20 部分,可能令人困惑:" 以及类类型的非类型模板参数及其子对象中的引用或指针类型的非静态数据成员(自 C++20 起)".用完整的报价更新了我的问题。

标签: c++ templates pointer-to-member


【解决方案1】:

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

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

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

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

【讨论】:

  • 我明白了,谢谢。 “指向成员的指针不像常规指针。它们指定成员,而不是对象。” - 那么将指向数据成员的指针称为constexpr 是否有意义?如果是这样,您是否知道标准(在某种程度上)是否在某处明确描述了这一点?
  • @dfri - 你可能会问,所以我查了一下。标准中的常量表达式由它不能是什么来定义,而不是它是什么。所以当你写&amp;Foo::foo_ 时,你会得到一个指定成员的prvalue。当放置在需要常量表达式的上下文中时,该纯右值会经历gauntlet of checks。只有通过检查的才是常量表达式。
  • 啊,我明白了,“排除其他”-standardese 解释了为什么我在发布之前很难通过标准和我自己来解决这个问题在这里。谢谢!
【解决方案2】:

正如 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(这是第一个函数参数)持有一个指向类实例的指针(在我们的例子中是this)。因为Foo::foo_ 是第一个成员,所以它的地址与其类匹配,所以&amp;f== &amp;f.foo_(其中f 是Foo 的一个实例)。因此,对于Foo::foo_,我们只需获取this 的地址并将该地址指向的字节加1。

第二种情况类似。唯一的区别是Foo::bar_ 是类中的第二个数据成员,由于Foo::foo_ 只占用1 个字节的空间,Foo::bar_ 位于reinterpret_cast&lt;char*&gt;(this) + 1,这由rdi + 1 反映。

【讨论】:

  • StoryTeller 回答的一个很好的补充,谢谢。
猜你喜欢
  • 1970-01-01
  • 2015-01-16
  • 2013-02-15
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-03-30
相关资源
最近更新 更多