Можно ли объявить виртуальное статическое постоянное значение в классе С++?

Я хотел бы иметь базовый класс с постоянным полем (например, уникальный идентификатор, связанный с классом, который нельзя изменить после компиляции). До сих пор объявление static const было бы в самый раз. Теперь я хотел бы унаследовать этот базовый класс и убедиться, что дочерние элементы этого класса имеют то же поле, но со своими собственными значениями. Как я могу это сделать?

Скажем, я хотел бы иметь базовый класс с именем Base с полем ID, которое содержит значение int, равное 0. Затем я хотел бы иметь классы A, B и C, все они являются публичными дочерними элементами Base. и я хотел бы убедиться, что у этих детей также будут поля ID с соответствующими значениями 1, 2 и 3 (под «убедиться» я имею в виду что-то вроде получения ошибки компилятора, если у них нет идентификатора заявлено явно).

Если бы мне удалось построить этот сценарий, я бы ожидал, что при запросе поля ID указателя Base* я должен получить разные значения в зависимости от того, был ли указатель создан как new A(), new B() или new C().

Мое предположение состояло бы в том, чтобы объявить ID как virtual static const, что, конечно, не имеет смысла и дает ошибку компилятора.

Но тогда что я могу сделать, чтобы добиться описанного результата? (Единственное, что я мог себе представить, это объявить ID виртуальной функцией, возвращающей целое число, а затем жестко закодировать значение в теле функции, но я ищу что-то более элегантное.)

Заранее спасибо!


person Siska Ádám    schedule 06.06.2012    source источник
comment
Полиморфизм работает только для функций-членов и недействителен для элементов данных.   -  person Mahesh    schedule 06.06.2012
comment
Я написал альтернативу на основе шаблонов: stackoverflow.com/a/36797199/1529139   -  person 56ka    schedule 22.04.2016


Ответы (2)


Метод static не может быть virtual, а элементы данных не могут быть virtual.

Но вы можете скрыть поля static в производных классах и использовать метод virtual для их возврата.

class A
{
public:
    static const int ID = 0;
    virtual int getID() { return A::ID; }
};
class B : A
{
public:
    static const int ID = 1;
    virtual int getID() { return B::ID; }
};

Альтернатива:

class A
{
public:
    A(int id = 0) : ID(id) {}
    const int ID;
    getID() { return ID; }
};
class B : public A
{
public:
    B() : A(1) {}
};
person Luchian Grigore    schedule 06.06.2012
comment
Да, это единственное решение, которое я знал до сих пор. Но нет ли способа решить это более элегантным/простым способом? Или, если это единственный путь, то мой вопрос скорее таков: по какой причине виртуальное наследование, которое работает с функциями, не будет работать с данными-членами? Я понимаю, почему на самом деле static virtual не имеет смысла, но я не могу понять, почему, скажем, простое поле virtual const int не имеет смысла... - person Siska Ádám; 06.06.2012
comment
@SiskaÁdám, потому что полиморфное поведение не определено для элементов данных. Так устроен язык. И это хорошо, имхо. - person Luchian Grigore; 06.06.2012
comment
@ SiskaÁdám Я разместил альтернативу, но первая мне все же кажется лучше. - person Luchian Grigore; 06.06.2012
comment
Что за бред, в C++11 можно даже сделать виртуальную функцию constexpr. Что не очень полезно, так как не удастся скомпилировать, когда вы действительно используете виртуальное наследование. Хотя f<A().getID()>(); на самом деле компилируется, учитывая, что f является шаблонной функцией, принимающей целое число... - person Damon; 06.06.2012
comment
@LuchianGrigore Понятно. Это был тот момент, который я, вероятно, пропустил. Однако не могли бы вы объяснить в нескольких словах, почему это хорошо? Я просто не могу вспомнить пример, в котором полиморфизм элементов данных мог бы привести к большим проблемам, чем полиморфизм функций. Спасибо, Адам - person Siska Ádám; 06.06.2012
comment
@SiskaÁdám обычно члены обозначают состояние. Если вы используете наследование, вы предполагаете, что класс B является классом A и имеет все его члены плюс другие, но у него есть члены A. Не переопределить. Они могут быть разными, но не переопределены. Я не могу найти пример, где это было бы полезно, кроме того, который вы опубликовали, что, вероятно, не является хорошим дизайном - есть другой механизм, который можно использовать для определения типа объекта во время выполнения (см. RTTI). Эта ID штука похожа на взлом. Может быть, это только я, потому что я не привык к концепции полиморфизма членов данных, но я, честно говоря, не вижу никакой пользы. - person Luchian Grigore; 06.06.2012
comment
@LuchianGrigore Спасибо за помощь. На самом деле удостоверение личности было просто фиктивным примером. В моем реальном сценарии у меня есть абстрактный базовый класс для команд рисования, а унаследованные классы будут фактическими командами рисования. Из соображений эффективности координаты, используемые командами рисования, хранятся отдельно в массиве (имея все точки одну за другой в блоке памяти, я могу быстрее выполнять преобразования), а команды рисования просто хранят указатель на свои данные. Унаследованная константа, о которой я мечтал, — это фактическое количество точек, требуемых различными командами рисования. - person Siska Ádám; 06.06.2012
comment
@ SiskaÁdám Я думаю, что если вам нужно что-то подобное, ваш дизайн неисправен. Но это было бы слишком широким вопросом для SO. Я предлагаю вам прочитать Head First Design Patterns (без принадлежности). - person Luchian Grigore; 06.06.2012
comment
@Damon: Вы имеете в виду виртуальное наследование или простое наследование и использование виртуальной функции? Это совсем другие. Во всяком случае, constexpr для виртуальной функции имеет смысл в тех случаях, когда динамическая диспетчеризация отключена (либо окончательный переопределяющий код известен в месте вызова во время компиляции, либо динамическая диспетчеризация явно отключена). - person David Rodríguez - dribeas; 06.06.2012
comment
@DavidRodríguez-dribeas: имеется в виду первое, например. A a; [...] f<a.getID()>;. Это (по довольно очевидной причине) не удастся скомпилировать (в конце концов, это не доказуемо константа времени компиляции). Это безумие, что вы все еще можете сделать виртуальную функцию constexpr, и она действительно работает так, как задумано, со статической диспетчеризацией (например, f<A().getID()>;). - person Damon; 06.06.2012
comment
@Damon: Так что это не первое, а второе: простое наследование и виртуальные функции (виртуальное наследование - это совершенно другой зверь). Насколько я знаю, ваш пример должен скомпилироваться. Поскольку a находится в той области, в которой к нему осуществляется доступ (т.е. вызов getID() не выполняется через ссылку/указатель), это не требует динамической диспетчеризации, а это означает, что соответствующее переопределение может быть обнаружено во время компиляции и что позволит встраивание функция в C++03, что, в свою очередь, означает, что не должно быть проблем с использованием этого конкретного вызова в качестве constexpr. - person David Rodríguez - dribeas; 06.06.2012
comment
На самом деле стандарт явно запрещает использование constexpr в виртуальных методах. Некоторые компиляторы могут по-прежнему разрешать это, но это не стандарт. - person Meta; 25.09.2016

Действительно, было бы очень полезно иметь виртуальные статические члены в C++. Их можно легко добавить в язык, новые ключевые слова не нужны. Следующий код, например, называет типы фигур для графической библиотеки:

class Shape {
public:
    static constinit virtual std::string name = delete;
    static constexpr virtual bool closed = delete;
    ...
};

class Circle : public Shape {
public:
    static constinit std::string name override { "circle" };
    static constexpr bool close override { true };
    ...
};

class Line : public Shape {
public:
    static constinit std::string name override { "line" };
    static constexpr bool close override { false };
    ...
};

Это объявляет Shape абстрактным базовым классом, поскольку построение Shape::name и Shape::closed явно пропускается через = delete.

Пространство для виртуальных статических элементов может быть выделено в той же VTable, которая уже используется для вызовов виртуальных функций. Если все виртуальные статические элементы имеют constinit (новое в C++20) или constexpr, то VTable может быть записан в постоянную память, где большинство компиляторов также записывают ее в настоящее время. Если нет, то вместо этого VTable следует поместить в память для чтения и записи.

Как правило, виртуальные статические элементы не обязательно должны быть const, они также могут быть доступны для чтения и записи.

Доступ к виртуальным статическим членам можно получить как с помощью имени класса в качестве префикса (где они будут вести себя как обычные статические члены), так и через объект, где указатель VTable объекта будет использоваться для доступа к нужной VTable.

Пока их нет в стандарте, их можно эмулировать с помощью виртуальных функций, которые возвращают ссылку на локальную статическую переменную:

virtual const std::string& get_name() const {
    static const std::string name { "circle" };
    return name;
}

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

person Kai Petzke    schedule 20.10.2020