Допустимо ли для стандартной реализации библиотеки иметь определение класса, отличное от стандарта С++?

Следующий код успешно скомпилирован с clang и MSVC, но не скомпилирован в GCC 6.1.0.

#include <memory>

template<typename R, typename T, typename... Args>
T* test(R(T::*)(Args...) const)
{
    return nullptr;
}

int main()
{
    using T = std::shared_ptr<int>;
    T* p = test(&T::get);
}

со следующим сообщением об ошибке

prog.cc: In function 'int main()':
prog.cc:13:16: error: invalid conversion from 'std::__shared_ptr<int, (__gnu_cxx::_Lock_policy)2u>*' to 'T* {aka std::shared_ptr<int>*}' [-fpermissive]
     T* p = test(&T::get);
            ~~~~^~~~~~~~~

Проблема заключается в том, что libstdС++ реализовал std::shared_ptr путем наследования функции-члена get из базового класса std::__shared_ptr.

В стандарте С++ 20.8.2.2 Шаблон класса shared_ptr он определяет определение класса класса std:: shared_ptr со всеми функциями-членами этого класса.

Мой вопрос заключается в том, должна ли реализация, по крайней мере, предоставлять все публичные члены класса, определенные в стандарте внутри стандартного класса? Разрешено ли предоставлять функции-члены путем наследования из базового класса, реализованного в libstdС++?

Ответ 1

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

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

Наследование члена - это почти то же самое, что и объявление его в вашем основном классе. Действительно, единственный способ узнать разницу - сделать то, что вы здесь сделали: использовать правила вывода аргументов шаблона. Даже если вы можете указать член как Derived::get, если он действительно исходит из некоторого базового класса, компилятор будет знать.

Однако, [member.functions] приходит сюда в GCC. Он имеет явный язык, позволяющий стандартным реализациям библиотек добавлять дополнительные перегрузки в класс. Из-за этого использование std::shared_ptr<int>::get здесь не является корректным поведением. Действительно, сноска 187 разъясняет это:

Следовательно, адрес функции-члена класса в стандартной библиотеке С++ имеет неопределенный тип.

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

Таким образом, хотя определение класса в стандартной библиотеке является нормативным текстом, [member.functions] дает понять, что единственное, что вы можете гарантировать об этих определениях, это то, что вы можете вызывать эти функции, используя предоставленные аргументы. Все, что угодно, например, получение указателей на элементы, определяется реализацией.