5
votes

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 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__.

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()?

2
The "since C++17" part quoted looks fishy. For information, the latest draft of the Standard allows a non-type template parameters of pointer type being the address of a non-static class member.YSC
@YSC thanks; I omitted, perhaps confusingly, the C++20 part from my quote: "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)". Updated my question with the full quote.dfrib

2 Answers

3
votes

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.

1
votes

As StoryTeller already mentioned, pointer to members are different from normal pointers. If we take a look at the (almost) unoptimized assembly generated by clang (full code here) we see the instantiations of the template:

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

Since those are member functions rdi (which is the first function argument) holds a pointer to the class instance (this in our case). Because Foo::foo_ is the first member, it's address matches with its class, so &f== &f.foo_ (where f is an instance of Foo). So for Foo::foo_ we simply take the address of this and increment the byte to which this address is pointing by 1.

The 2nd case is similar. The only difference is that Foo::bar_ is the second data member in the class and since Foo::foo_ takes only 1 byte of space, Foo::bar_ is located at reinterpret_cast<char*>(this) + 1 which is reflected by the rdi + 1.