Приведенный ниже код основан на Основные рекомендации CPP, Приложение C, «Обсуждение: используйте фабричную функцию, если вам нужно «виртуальное поведение» во время инициализации».
Идея кода заключается в использовании фабричной функции для создания экземпляров производных классов, которые требуют вызова «пост-конструктора» в производном классе.
Код у меня такой:
#include <iostream>
#include <memory>
using namespace std;
class B {
protected:
class Token {};
public:
// constructor needs to be public so that make_shared can access it.
// protected access level is gained by requiring a Token.
explicit B(Token) { /* ... */ } // create an imperfectly initialized object
template<class T>
static shared_ptr<T> create() // interface for creating shared objects
{
auto p = make_shared<T>(typename T::Token{});
p->post_initialize();
return p;
}
private: //change to protected and it compiles in C++ Builder
void post_initialize() // called right after construction
{ /* ... */ f(); /* ... */ } // GOOD: virtual dispatch is safe
virtual void f() = 0;
};
class D : public B { // some derived class
protected:
class Token {};
public:
// constructor needs to be public so that make_shared can access it.
// protected access level is gained by requiring a Token.
explicit D(Token) : B{ B::Token{} } {}
D() = delete;
protected:
//Make B:create() a friend so it can access private and protected members...
template<class T>
friend shared_ptr<T> B::create();
private:
void f() override {
std::cout << "I'm a D" << std::endl;
};
};
int main() {
shared_ptr<B> p = B::create<D>(); // creating a D object
return 0;
}
Этот код компилируется и запускается с использованием MinGW версии w64 9.0 и MSVC 2022. Однако C++Builder 11.5 Alexandria, компилятор Clang, 32-разрядный и 64-разрядный, жалуется:
ошибка: 'post_initialize' является закрытым членом 'B'
Но если я изменю раздел B
класса private:
на protected:
, C++Builder скомпилирует код.
Я ожидал, что C++Builder скомпилирует это, так как B::create()
должен иметь возможность вызывать private
функцию B
, не так ли?
Или я ошибаюсь, и MinGW и MSVC неправильно умеют это компилировать?
Если вы удалите объявление Token
из класса D
, вам даже не нужно объявлять B::create
другом, потому что ему не нужно будет обращаться ни к одному элементу D
. И D
конструктор по-прежнему защищен B::Token
.
Токен необходим, чтобы предотвратить прямое создание экземпляра класса, но при этом оставить конструктор общедоступным, чтобы make_shared мог получить к нему доступ. Мне пришлось несколько раз перечитать код (который взят из основных правил CPP), прежде чем я понял, почему это так. Идея состоит в том, что базовый класс после создания экземпляра всегда вызывает конструктор сообщений в производных классах, не полагаясь на то, что пользователь сделает это, что было бы в случае, если бы пользователь мог вызвать конструктор напрямую...
Но пользователь не сможет вызвать конструктор напрямую, даже если вы удалите Token
из D
, потому что ему понадобится Token
из B
, что также является protected
.
@sklott Да, ты прав. Если я изменю конструктор D на явный D(B::Token) : B{ B::Token{} } {}, тогда я могу удалить объявление друга. И C++ Builder также может его скомпилировать. Похоже, ошибка в том, что C++ Builder неправильно понял выражение друга. Если я оставлю оператор друга в коде, даже с этим улучшением C++ Builder все равно не сможет его скомпилировать...
Какую версию Clang использует C++ Builder? Clang 14.0.0 принимает этот код.
@JerryCoffin Используется версия 5.0.2. Они хотят перейти на v14 или v15 для следующего крупного выпуска. Это далеко от последнего Clang — я не знаю, почему это так, но, вероятно, это связано с их расширениями компилятора, необходимыми для поддержки библиотек Delphi.
@AndyB: Да, обратная совместимость может объяснить большую мотивацию.
Я ожидал, что C++ Builder скомпилирует это, поскольку B::create() должен иметь возможность вызывать частную функцию B, не так ли?
Да, post_initialize
разрешено использовать/вызывать как p->post_initialize()
изнутри B::create()
, поскольку оба принадлежат к одному классу.
Судя по вашему описанию ошибки, это похоже на ошибку C++ Builder.
Джейсон, спасибо, что подтвердили, что я не схожу с ума :-) Я обнаружил ошибку в Embarcadero...
Embarcadero сообщает, что это проблема Clang — до версии 7.1.0 код не компилируется. Начиная с версии 8.0 он компилируется. В настоящее время они находятся в процессе обновления компилятора C++ Builder C++ до CLang V14 или V15 для будущего выпуска...
@AndyB Понятно. Вы можете поделиться здесь ссылкой на эту ошибку, чтобы будущие читатели также могли найти ее здесь.
Я попытался поднять его через портал сообщений о проблемах Embarcadero, но он не работал, поэтому вместо этого я обратился в службу поддержки. Они поднимут ошибку и пришлют мне ссылку, которую я опубликую, как только получу.
Они добавили эту проблему в свою систему отслеживания ошибок Jira как проблему RS-115956. Это проблема Clang, которая уже исправлена в более поздних выпусках Clang и будет решена не позднее, чем в следующем крупном выпуске C++ Builder. Но, как показал @sklott в комментариях выше, этот код можно изменить, чтобы устранить необходимость в объявлении друга, и тогда проблема исчезнет.
Похоже на ошибку в C++ Builder.