Почему указатели на функции-члены отличаются от обычных указателей на функции в C++?

В начале был C.
И C имеет структуру, выражения и функции для их упаковки. И это было хорошо.
Но C также использовал goto и switch case и синтаксис, который следовал за использованием, так что, возможно, не так хорошо.

У этого также были указатели, вызывая много скрежетаний зубов из-за алиасов и указателей!
Но он также имел указатели на функции, позволяющие отправлять время выполнения, и за ним следовало много радости.
Теперь данные могут диктовать код, а также данные, диктующие код, и оба они были первого класса (или близкими).
На все, на что можно было бы указать, можно указать, с тем же указателем: святая пустота *.

И все были равны во славе.

Затем появился C++ и связал данные и код в Object.
И вот, это был просто синтаксический сахар, потому что функция и метод не так уж отличаются,
(независимо от того, что Солнце или Оракул могут вам сказать).

Obj-> Foo (int val), являющийся (приблизительно) таким же, как Foo(Obj* this, int val), они все еще были равны,
под святой пустотой *.

И затем, с наследованием, возникла борьба, поскольку внешний производный класс может добавить к внутренней базе.
Но вот, в эти простые времена было найдено решение: поставить каждую базу перед производным.
Затем одним и тем же указателем мы можем указать и на ребенка, и на отца.

И все же на все, на что можно было бы указать, можно было бы указать святой пустотой *.

С Виртуальным мы потеряли нашу простоту и бродили целую вечность.
Хотите знать, как обращаться с бриллиантами или кругами, которые не совсем эллипсы.
Но даже когда мы отбрасываем наших старых арендаторов C, каждая инструкция сокращается до простого asm,
мы поняли, что некоторые вещи должны выглядеть просто (даже если они сложны).

Итак, мы посмотрели на секретные VTables, которые возникли, и подумали "достаточно хорошо".
В то время как мы ввели скрытые данные, мы уменьшили сложность.
И теперь любой вызов, сделанный через виртуал, был перенаправлен через VTable.
И поскольку на все подобъекты класса можно указывать одним указателем, этого было достаточно.

Но даже несмотря на то, что метод отправки изменился, мы все же могли указывать на все, со святой пустотой *.

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

И так, настройки указателя должны быть сделаны, с каждым Виртуальным вызовом.
Потому что базовый класс действительно может быть скрытым MI-производным классом, и его необходимо скорректировать.
Так что за поддержку Choice Few, которые использовали MI, мы все заплатили цену.

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

Зверю требовался собственный синтаксис, потому что никому другому не хватило бы.
И этот синтаксис, так редко используемый, будет иметь такой низкий приоритет, что потребует использования parans при каждом использовании.
Хотя в священном стандарте злой совет решил разрешить бросать такие жалкие вещи,
но когда приведение от типа к типу, не вызывается без вызова поведения наиболее неопределенным!

И нет, декадентские с синтаксисом и жадные, они были толстыми и не могли вписаться в пустоту *.
В качестве единственного способа узнать, на какой объект указывать, был
Расположенный глубоко в указателе, и проверяется с каждым поиском VTable.

Но это, братья, не так, как должно быть.
Эта сложность реализации проистекает из самых специфических решений.

class Base1
{
public:
    virtual void foo();
};

class Base2
{
public:
    virtual void bar();
};

class Derived: public Base1, public Base2
{
public:
    void unrelated();
}

Как видно здесь, Derived* должен быть скорректирован при вызове foo() или bar(); он не может указывать на Base1 и Base2 одновременно, как в случае простого одиночного наследования. Действительно, невозможно правильно предсказать, какая часть смещения необходима при вызове из базового класса, поэтому у большинства есть какой-то механизм для добавления его в vtable.

Тем не мение:

class Derived: public Base1, public Base2
{
public:
    void unrelated();

    virtual void foo() { Base1::foo(); }
    virtual void bar() { Base2::bar(); }
}

Решает проблему, не внося изменений в исходную объектную модель!
Поскольку каждый метод теперь существует, он может быть правильно добавлен в vtable, и при вызове точно знает, сколько нужно настроить указатель, позволяя продолжить вызов без какого-либо вреда!
И теперь, как приведение указателя на функцию-член, так и вызов его при приведении, хорошо определены!

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

Если бы мы жили в таком мире грез.

К сожалению для нас, мы живем с толстыми указателями на функции-члены, виртуальными таблицами, которые должны корректировать каждый вызов, и указателями на функции-члены, которые не могут надлежащим образом создавать делегатов или многие другие полезные шаблоны. В MSVC они меняют размер, когда вы их разыгрываете!

Проблема настолько велика, что std::function может выделять память динамически в большинстве реализаций из-за различных проблем, описанных здесь. Использование thunks для не переопределенных методов, как я уже подробно описал, решило бы эту проблему довольно легко, и стоимость этого - несколько встроенных скрытых методов и некоторые vtable-изменения, наряду с возможным (но незначительным) снижением скорости. виртуальная диспетчеризация, в случае, если функция не переопределена и также не может быть встроена идеально.

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

Фактически, как отмечалось в документе " Самые быстрые возможные делегаты", текущее решение фактически замедляет каждый виртуальный вызов; он вызывает дополнительную проверку и дополнительное использование памяти через жирные указатели, которые даже для одиночного наследования должны хранить дополнительные данные (или рискуют их потерять, как могут указатели на функции-члены MSVC). Это явно не в философии C++ "плати, если ты используешь, не, если не будешь"!

Итак, еще раз, почему указатели на функции-члены отличаются от "свободных" указателей на функции? Есть ли логическая причина, почему они не являются просто указателями на функции со специальным соглашением о вызовах или с дополнительным аргументом для "this"?

1 ответ

В C++ есть правило "не плати за то, что ты не используешь", означающее, что нормальные операции не должны замедляться, чтобы оплачивать другие языковые функции, которые не использует программист. Хотя вы абсолютно могли бы сделать указатели на функции-члены такими же, как обычные указатели на функции, дополнительные издержки от учета динамической диспетчеризации, разных смещений vtable, разных смещений базовых объектов и thunks и т. Д. Привели бы к дополнительным накладным расходам в указателях обычных функций, либо из-за дополнительная память, необходимая для хранения этой информации (больший размер) или дополнительная логика, необходимая для отправки (дополнительное время и сгенерированный код). Поэтому имеет смысл разделить указатели на функции и указатели на функции-члены на отдельные типы с отдельными реализациями.

Надеюсь это поможет!

Другие вопросы по тегам