Почему gcc не может девиртуализировать вызов этой функции?

#include <cstdio>
#include <cstdlib>
struct Interface {
    virtual void f() = 0;
};

struct Impl1: Interface {
    void f() override {
        std::puts("foo");
    }
};

// or __attribute__ ((visibility ("hidden")))/anonymous namespace
static Interface* const ptr = new Impl1 ;

int main() {
    ptr->f();
}

При компиляции с g++ - 7 -O3 -flto -fdevirtualize-at-ltrans -fipa-pta -fuse-linker-plugin вышеупомянутый ptr->f() не может быть девиртуализирован.

Кажется, что никакая внешняя библиотека не может изменять ptr. Является ли это недостатком оптимизатора GCC или потому, что некоторые другие источники делают недоступным в этом случае девиртуализацию?

Ссылка Godbolt

UPDATE: Кажется, что clang-7 с -flto -O3 -fwhole-program-vtables -fvisibility=hidden является единственным флагом компилятора + (как в 2018/03), который может девиртуализировать эту программу.

Ответ 1

Если вы переместите ptr в основную функцию, результат будет очень информативным и даст сильный намек на то, почему gcc не хочет де виртуализировать указатель.

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

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

На самом деле это еще хуже. Если вы добавите локальную переменную, важно, возникает ли вызов функции f на статическом указателе между созданием локальной переменной и вызовом f или нет. Узел, показывающий f вставленного случая здесь: Еще одна связи godbolt; и просто перестроить его самостоятельно на сайте, чтобы увидеть, как сборка превращается в строку f когда другой вызов не вставлен.

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

clang делает то же самое. Это кажется слишком осторожным для меня, но я не писатель-компилятор.

Ответ 2

Я только что сделал другой эксперимент, как и во всём ответе. Но на этот раз я делаю указатель на статический объект:

Impl1 x;
static Interface* const ptr = &x ;

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

Разрешено изменять объект по адресу, на который указывает ptr. Поэтому компилятор должен отслеживать, что происходит на этом адресе, чтобы узнать, каков фактический динамический тип объекта. Поэтому я считаю, что разработчик оптимизатора, возможно, счел, что отслеживание того, что происходит в куче, будет слишком сложно в реальной программе, поэтому компилятор просто этого не сделает.