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

Следующий код успешно скомпилирован с помощью 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);
            ~~~~^~~~~~~~~

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

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

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


person kwanti    schedule 24.06.2016    source источник
comment
17.6.5.11 Производные классы [происхождение] Реализация может наследовать любой класс в стандартной библиотеке C ++ от класса с именем, зарезервированным для реализации. Сноска 188 (№ 3797) также может быть интересна.   -  person Revolver_Ocelot    schedule 24.06.2016
comment
Хмя, вы также не найдете в стандарте аргумента шаблона Lock_policy. Это то, что происходит, когда разработчики библиотеки компилятора либо реализуют класс до утверждения стандарта, либо делают его практичным для использования в реальном мире. Не включать политику блокировки в стандарт было довольно спорным решением. Реальный мир отменяет.   -  person Hans Passant    schedule 24.06.2016
comment
Я не очень хорошо читаю стандарт. Насколько я понимаю, это предложение состоит в том, что реализации разрешено наследование от базового класса, но в нем не указывается, разрешено / запрещено предоставлять функции-члены класса в стандартном классе через наследование от базового класса.   -  person kwanti    schedule 24.06.2016
comment
Проблема в том, что компилятор не прав. std::shared_ptr<int>::get является функцией-членом std::shared_ptr<Int> независимо от того, как он туда попал, а T в шаблоне - std::shared_ptr<int>.   -  person Pete Becker    schedule 24.06.2016
comment
Вы также можете взглянуть на стандартные заголовки MSVC, они тоже не совсем такие же, как стандартные. Например, их реализация std::exception позволяет построить его из строки C, которую exception.what() вернет; насколько мне известно, ни один из производных от него классов (таких как std::logic_error и std::runtime_error) даже не отменяет what(), они просто передают строковый параметр своих ctors специфичному для MS exception ctor, который принимает строку C для установки вывода what(). Важно то, что он предлагает поведение, указанное в стандарте.   -  person Justin Time - Reinstate Monica    schedule 24.06.2016
comment
Кроме того, большинство их реализаций стандартных классов являются производными от скрытых классов, что более точно соответствует поведению GCC.   -  person Justin Time - Reinstate Monica    schedule 24.06.2016
comment
Я думаю, что для реализации допустимо предоставить дополнительный конструктор или метод, а реализация является производной от скрытого класса, но реализованный стандартный класс должен, по крайней мере, иметь все общедоступные члены, определенные в стандарте. Если в стандарте указано, что в определении класса shared_ptr есть член get, могу ли я ожидать правильного поведения при компиляции приведенного выше кода?   -  person kwanti    schedule 25.06.2016


Ответы (1)


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

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

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

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

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

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

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

person Nicol Bolas    schedule 25.06.2016
comment
Я думаю, что сноску 187 следует читать в контексте [member.function] 2 и (2.1). 2. Реализация может объявлять дополнительные невиртуальные сигнатуры функций-членов внутри класса: (2.1) - путем добавления аргументов со значениями по умолчанию в сигнатуру функции-члена; 187 Насколько я понимаю, реализация может предоставить член функция void get(int = 0); если std указывает функцию-член void get() Следовательно, в сноске 187 поясняется, что тип элемента get не указан. Это может быть void(T::*)(int) или void(T::*)(), но разве это не должно быть внутри класса? - person kwanti; 25.06.2016
comment
если приведенная выше тестовая функция возвращает sizeof(Args...), то в сноске 187 она определяется реализацией. - person kwanti; 25.06.2016
comment
@kwanti: в сноске не говорится, что подпись определяется реализацией; он сказал, что весь тип будет. - person Nicol Bolas; 25.06.2016
comment
Поскольку это единственный ответ, я приму его (спасибо за ответ). Невероятно, что стандарт говорит нам, что мы не можем использовать указатель на функцию-член стандартного класса ни для чего, потому что мы не знаем его типа. Правильно ли я говорю, что это означает, что если я использую std :: function для привязки к указателю функции-члена любого стандартного класса, я не могу ожидать, что он будет переносимым, потому что тип (включая сигнатуру функции?) Определяется реализацией? - person kwanti; 29.06.2016
comment
@kwanti: Довольно много. Вы всегда можете использовать лямбда для создания эффекта вызова указателя на член. - person Nicol Bolas; 29.06.2016
comment
Будет интересно посмотреть, какой тип функции-члена стандартного класса мы получили, когда C ++ имеет отражение. :-) - person kwanti; 29.06.2016