Почему размер указателя на функцию отличается от размера указателя на функцию-член?

Не является указателем только адрес? Или мне что-то не хватает?

Я тестировал несколько типов указателей:

  • указатели на любые переменные одинаковы (8B на моей платформе)
  • указатели на функции имеют тот же размер, что и указатели на переменные (8B)
  • указатели на функции с разными параметрами - все те же (8B)

BUT указатели на функции-члены больше - 16B на моей платформе.

Три вещи:

  • Почему указатели на функции-члены больше? Какая еще информация нужна им?
  • Насколько я знаю, стандарт ничего не говорит о размере указателя кроме, который void* должен иметь возможность "содержать" любой тип указателя. Другими словами, любой указатель должен быть отброшен на void*, правильно? Если да, то почему sizeof( void* ) равно 8, а sizeof указатель на функцию-член равен 16?
  • Есть ли еще примеры для указателей, которые имеют разный размер (я имею в виду, для стандартных платформ, а не некоторых редких и специальных)?

Ответ 1

РЕДАКТИРОВАТЬ. Итак, я заметил, что я все еще получаю голоса в эти месяцы позже, хотя мой первоначальный ответ плох и обманчив (я даже не могу вспомнить, что я думал в то время, и это не имеет большого смысла!), поэтому я решил попробовать и прояснить ситуацию, так как люди все равно должны добраться сюда через поиск.

В самой нормальной ситуации вы можете в значительной степени подумать о

struct A { int i; int foo() { return i; } };
A a; a.foo();

а

struct A { int i; };
int A_foo( A* this ) { return this->i; }; 
A a; A_foo(&a);

(Начиная выглядеть как C, правильно?) Итак, вы думаете, что указатель &A::foo будет просто таким же, как обычный указатель на функцию. Но есть несколько осложнений: множественное наследование и виртуальные функции.

Итак, представьте, что у нас есть:

struct A {int a;};
struct B {int b;};
struct C : A, B {int c;};

Это может быть сделано следующим образом:

Multiple inheritance

Как вы можете видеть, если вы хотите указать на объект с помощью A* или C*, вы указываете на начало, но если вы хотите указать на него с помощью B*, вы должны указать где-то посередине. Поэтому, если C наследует некоторую функцию-член из B, и вы хотите указать на нее, затем вызовите функцию в C*, ее необходимо знать, чтобы перетасовать указатель this. Эта информация должна быть где-то сохранена. Таким образом, он включается с указателем функции.

Теперь для каждого класса, имеющего функции virtual, компилятор создает список из них, называемый виртуальной таблицей. Затем он добавляет дополнительный указатель на эту таблицу в класс (vptr). Итак, для этой структуры класса:

struct A
{
    int a;
    virtual void foo(){};
};
struct B : A
{
    int b;
    virtual void foo(){};
    virtual void bar(){};
};

Компилятор может сделать это следующим образом: enter image description here

Таким образом, указатель функции-члена для виртуальной функции на самом деле должен быть индексом в виртуальной таблице. Таким образом, указатель функции-члена действительно нуждается в 1) возможно, указателе функции, 2) возможно, настройке указателя this и 3), возможно, индексе vtable. Чтобы быть последовательным, каждый элемент-указатель функции должен быть способен на все это. Так что 8 байтов для указателя, 4 байтов для настройки, 4 байтов для индекса, для 16 всего байтов.

Я считаю, что это очень многое зависит от компиляторов, и есть много возможных оптимизаций. Вероятно, никто не реализует его так, как я описал.

Подробные сведения см. в this (перейдите к "Реализации указателей функций членов" ).

Ответ 2

В основном потому, что они должны поддерживать полиморфное поведение. См. Статью Раймонда Чена.

Ответ 3

Некоторые объяснения можно найти здесь: Основное представление указателей функций-членов

Хотя указатели на элементы ведут себя как обычные указатели, за кулисами их представление совершенно иное. Фактически, указатель на член обычно состоит из структуры, содержащей до четырех полей в определенных случаях. Это связано с тем, что указатели на члены должны поддерживать не только обычные функции-члены, но также функции виртуальных членов, функции-члены объектов, которые имеют несколько базовых классов, и функции-члены виртуальных базовых классов. Таким образом, простейшая функция-член может быть представлена ​​как набор из двух указателей: один, содержащий физический адрес памяти функции-члена, и второй указатель, который удерживает этот указатель. Однако в таких случаях, как виртуальная функция-член, множественное наследование и виртуальное наследование, указатель на член должен хранить дополнительную информацию. Поэтому вы не можете накладывать указатели на участников обычным указателям и не можете безопасно использовать между указателями члены разных типов. Blockquote

Ответ 4

Я бы предположил, что это имеет какое-то отношение к указателю this... То есть каждая функция-член также должна иметь указатель на класс, в котором они находятся. Затем указатель делает функцию немного большей по размеру.

Ответ 5

Некоторые из основных причин представления указателей на функции-члены как {this, T (*f)()}:

  • У него более простая реализация в компиляторе, чем реализация указателей на функции-члены как T (*f)()

  • Это не связано с генерацией кода времени выполнения или дополнительной бухгалтерией

  • Он работает достаточно хорошо по сравнению с T (*f)()

  • Недостаточно спроса со стороны программистов на С++ для того, чтобы размер указателей на функции-члены был равен sizeof(void*)

  • Генерация кода времени выполнения во время выполнения де-факто является табу для кода С++ в настоящее время