Consider the following snippet:
#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();
}
My first guess would've been that this was ill-formed, as f
in callMeWhenever()
has automatic storage duration, and its address is not known at compile time, whereas the member template function increaseCounter()
of Foo
is instantiated with pointers to data members of Foo
, and the memory representation of a given class type is compiler specific (e.g. padding). However, from cppreference / Template parameters and template arguments, afaics, this is well-formed:
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__
.
How does this work? 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?
I.e./e.g., is the compile time pointer to the data member non-type template argument counter
in Foo::increaseCounter()
(for each of the two specific pointer to data member instantations) simply a compile time address offset for any given instantiation of Foo
, that will later become a fully resolved address for each instance of Foo
, even for yet to be allocated ones such as f
in the block scope of 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?
Pretty much. It's an "offset" even outside of a compile time context. Pointers to members are not like regular pointers. They designate members, not objects. That also means that the verbiage about valid pointer targets does not relate to pointer-to-members.
That's why to produce an actual lvalue out of them, one must complete the picture with something that refers to an object, such as we do in this->*counter
. If you were to try and use this->*counter
where a constant expression is required, the compiler would have complained, but it would have been about this
, and not counter
.
Their distinct nature is what allows them to uncondionally be compile time constants. There's no object that the compiler must check as a valid target.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With