Указатель на член (data) в качестве нетипичного параметра шаблона, например, с автоматическим c сроком хранения / без связи - PullRequest
5 голосов
/ 09 января 2020

Рассмотрим следующий фрагмент:

#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();
}

Моим первым предположением было бы, что это неправильно, поскольку f в callMeWhenever() имеет автоматическое c время хранения и его адрес неизвестен во время компиляции, в то время как функция шаблона члена increaseCounter() из Foo создается с указателями на члены данных Foo, а представление памяти данного типа класса задается компилятором c (например, заполнение) , Однако из cppreference / параметров шаблона и аргументов шаблона , на самом деле, это правильно:

Аргументы типа нетипизированные

Следующие ограничения применяются при создании экземпляров шаблонов, имеющих нетипичные параметры шаблона:

[..]

[ до C ++ 17 ] Для указателей на элементы: аргумент должен быть указателем на член, выраженным как &Class::Member, или константным выражением, которое оценивается как нулевой указатель или значение std::nullptr_t.

[..]

[ начиная с C ++ 17 ] Исключением является только то, что нетипичные параметры шаблона ссылки или тип указателя [ добавляются с C ++ 20: и non-stati c члены-данные ссылочного типа или типа указателя в нетиповом параметре шаблона типа класса и его подобъектов (начиная с C ++ 20) ] не могут ссылаться на / быть адресом из

  • подобъекта (включая элемент класса non-stati c, базовый подобъект или элемент массива);
  • временный объект (включая объект, созданный во время инициализации ссылки);
  • строковый литерал;
  • результат typeid;
  • или предопределенная переменная __func__.

Как это работает? Обязан ли компилятор (по прямым или косвенным, например, указанным выше, стандартным требованиям) самому разбираться с этим, сохраняя только смещения адресов (во время компиляции) между членами, а не фактические адреса?

Т.е. / Например, является ли указатель времени компиляции на аргумент шаблона нетипичного элемента данных counter в Foo::increaseCounter() (для каждого из двух указанных указателей c на экземпляры элементов данных) просто смещение адреса времени компиляции для любого данного экземпляра Foo, который впоследствии станет полностью разрешенным адресом для каждого экземпляра Foo, даже для еще не выделенных, таких как f в области действия блока callMeWhenever()?

Ответы [ 2 ]

3 голосов
/ 09 января 2020

Требуется ли компилятору (по прямым или косвенным, например, указанным выше, стандартным требованиям), чтобы самому разобраться с этим, сохраняя только смещения адресов (во время компиляции) между членами, а не фактические адреса?

В значительной степени. Это «смещение» даже вне контекста времени компиляции. Указатели на участников не похожи на обычные указатели. Они обозначают участников, а не объекты. Это также означает, что словосочетание о действительных указателях целей не относится к указателям на элементы.

Именно поэтому для получения из них действительного значения l необходимо дополнить рисунок чем-то это относится к объекту, как мы делаем в this->*counter. Если бы вы попытались использовать this->*counter там, где требуется постоянное выражение, компилятор жаловался бы, но это было бы примерно this, а не counter.

Их особая природа заключается в том, что позволяет им всегда компилировать постоянные времени. Нет объекта, который компилятор должен проверять как допустимую цель.

1 голос
/ 09 января 2020

Как уже упоминал StoryTeller, указатели на элементы отличаются от обычных указателей. Если мы посмотрим на (почти) неоптимизированную сборку, сгенерированную clang (полный код здесь ), мы увидим экземпляры шаблона:

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_ является первым членом, его адрес совпадает с его классом, поэтому &f== &f.foo_ (где f является экземпляром Foo). Поэтому для Foo::foo_ мы просто берем адрес this и увеличиваем байт, на который указывает этот адрес, на 1.

Второй случай аналогичен. Единственное отличие состоит в том, что Foo::bar_ является вторым элементом данных в классе и, поскольку Foo::foo_ занимает только 1 байт пространства, Foo::bar_ расположен в reinterpret_cast<char*>(this) + 1, что отражается rdi + 1.

...