指向(数据)成员的指针作为非类型模板参数,例如具有自动存储持续时间/无链接
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_t
value.
[..]
[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_
(其中 f
是 Foo
的一个实例)。因此,对于 Foo::foo_
,我们只需获取 this
的地址并将该地址指向的字节递增 1。
第二种情况类似。唯一的区别是 Foo::bar_
是 class 中的第二个数据成员,因为 Foo::foo_
只占用 space 的 1 个字节,所以 Foo::bar_
位于 reinterpret_cast<char*>(this) + 1
由 rdi + 1
.
反映
考虑以下片段:
#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 orstd::nullptr_t
value.[..]
[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_
(其中 f
是 Foo
的一个实例)。因此,对于 Foo::foo_
,我们只需获取 this
的地址并将该地址指向的字节递增 1。
第二种情况类似。唯一的区别是 Foo::bar_
是 class 中的第二个数据成员,因为 Foo::foo_
只占用 space 的 1 个字节,所以 Foo::bar_
位于 reinterpret_cast<char*>(this) + 1
由 rdi + 1
.