Динамическое изменение виртуального указателя во время выполнения
Допустим, у меня есть два класса, которые наследуют базовый класс, который имеет чисто виртуальную функцию. Оба класса реализуют свою собственную версию этой функции, но не добавляют дополнительных переменных-членов, поэтому они имеют одинаковый размер. Теперь иногда, во время выполнения программы, я хочу преобразовать один класс в другой, не копируя все его данные. В общем, я хочу, чтобы он использовал виртуальную таблицу другого класса. Есть ли портативный способ сделать это?
8 ответов
Портативный способ сделать это - реализовать собственную систему классов, в которой есть виртуальные указатели, которые можно скопировать.
В стандарте C++ нет такого понятия, как виртуальный указатель.
Один молодой коллега из Andersen Consulting (ныне Accenture) в Норвегии однажды обратился ко мне с серьезной проблемой. Их приложения, разработанные в Visual Basic, загружались очень долго. Он подозревал, что это может быть потому, что они помещают каждый класс в свою собственную DLL?
Опасаясь худшего, я поинтересовался дальше. И да, у них также были проблемы с произвольными сбоями и т. Д.
Он подозревал, что в противном случае необъяснимые сбои могут быть связаны с их оригинальной схемой изменения типа объекта во время выполнения путем замены указателя vtable?
Я предположил, что, может быть, они действительно не должны делать эти вещи. Он скептически посмотрел на меня и решился, что у них не было времени снова что-то делать с нуля. Фактически, они уже растягивали это, и с этим возникали различные проблемы, например, руководитель проекта настаивал на том, чтобы они работали на площадке клиента, а не участвовали в обязательных встречах. Для меня это звучало как управление грибами (держите их в темноте, когда голова всплывает, отрежьте ее): эти вещи часто идут вместе.
В любом случае, я даю вам тот же совет: не надо.
Возможно, вы можете вместо этого реализовать быстрые операции перемещения для перемещения данных из a в b?
Или, может быть, вы обнаружите, что это случай преждевременной оптимизации?
Ура & hth.,
Нету. Что касается языка, то нет такой вещи, как виртуальная таблица, не говоря уже о правилах того, как она выглядит / что она содержит / где она хранится.
Некоторая форма композиции, вероятно, больше подходит для вашей задачи.
Как говорилось в других ответах, изменение виртуальной таблицы на самом деле непереносимо.
Однако есть несколько обходных путей, которые могут позволить вам реализовать аналогичную семантику без фактического изменения типа класса:
Это простейшее решение состоит в том, чтобы "свернуть свое" наследование с помощью перечисления, описывающего текущую реализацию:
class MyClass
{
public:
enum DerivedType { A, B };
private:
DerivedType myType;
public:
void myVirtualFunction()
{
if (myType == A)
myAFunction();
else
myBFunction();
}
}
Вы также можете использовать указатель на функцию в качестве открытой переменной-члена, которая установлена на функцию, указывающую тип класса. Затем вы можете установить указатель на функцию другого класса, чтобы "изменить его тип"
Поскольку вы упоминаете, что хотите избежать копирования данных, вы можете сохранить свои разные классы, но иметь указатели подсчета ссылок на все переменные-члены, чтобы вы могли быстро создавать новые объекты противоположного типа друг от друга.
Есть ли портативный способ сделать это?
Точно нет. Специфика того, как реализуются виртуальные функции, не определяется спецификацией, и, следовательно, нет никакого портативного способа притворяться, что один виртуальный класс - это другой.
Как насчет использования размещения новых? Возможно, это не совсем переносимо, но оно делает именно то, что нужно - заменяет vtable и ничего более. Просто нужно позаботиться о конструкторе - используйте пустой.
struct Base
{
int someData;
virtual int GetValue() = 0;
};
struct A : public Base
{
int GetValue() override { return 11111; }
};
struct B : public Base
{
int GetValue() override { return 22222; }
};
A ob;
ob.someData = 123;
auto ob2 = new (&ob) B;
auto value = ob2->GetValue();
Не говоря уже об очевидных вещах, таких как размер классов, лучшие практики и т. Д.
Хотя этот вопрос старый, я хотел бы найти способ сделать это. (Не совсем уверен в портативности)
Из того, что я понимаю, у вас есть класс B
а также C
которые наследуют от некоторого класса A
и между ними существует только одна виртуальная функция. (Метод, который я здесь представляю, работает, если B
а также C
также не связаны.)
class A {
public:
virtual std::string hello() = 0;
};
class B : public A {
public:
virtual std::string hello() { return "B"; }
};
class C : public A {
public:
virtual std::string hello() { return "C"; }
};
И тогда вы хотите взять B
к C
затем позвоните hello
и получить "B"
,
Итак, есть способ создать разбавленную версию boost::any
что будет отбрасывать что угодно на что угодно, лишь бы оно подходило:)
struct parent {};
template< typename T >
struct child : public parent {
child(T const& t): item(t){}
mutable T item;
};
template< typename T >
T& as(parent const & p) { return static_cast< child< T > const& >(p).item; }
Тогда смешайте все это вместе:
B b;
parent* p = new child< B >(b);
std::cout << as< C >(*p).hello() << std::endl;
// ==== OUTPUT ====
// B
Можете увидеть код в действии здесь.
Чтобы продвинуться дальше, мы можем создать функцию, которая конвертирует из одного типа в другой, не давая заднему концу мошки о том, что происходит между ними.
template< typename TO, typename FROM >
TO& convert(FROM const& from) {
parent* p = new child< FROM >(from);
return as< TO >(p);
};
Это можно запустить здесь.
(Понял, что я пропустил наследование в этих примерах кодовых ссылок, но после прочтения вопроса я думаю, что это именно то, что было на самом деле желательно. Итак, чтобы посмотреть тест без наследования, перейдите сюда)
Какой-то другой код, с которым я начал играть, который, как я думал, мог бы также помочь некоторым...
#include <iostream>
#include <string>
class B {
public:
virtual char hello() {return 'B';}
};
class C {
public:
virtual int hello() {return 65;}
};
struct parent {};
template< typename T >
struct child : public parent {
child(T const& t): item(t){}
mutable T item;
};
template< typename T >
T& as(parent const & p) { return static_cast< child< T > const& >(p).item; }
template< typename TO, typename FROM >
TO& convert(FROM const& from) {
parent* p = new child< FROM >(from);
return as< TO >(*p);
};
int main()
{
B b;
std::cout << convert< C, B >(b).hello() << std::endl;
C c;
std::cout << convert< B, C >(c).hello() << std::endl;
}
// ==== OUTPUT ====
// 66
// A
Разобрался, как все это сделать в функции convert:
template< typename TO, typename FROM >
TO& convert(FROM const& from) {
struct parent {};
struct child : public parent {
child(FROM const& t): item(t){}
mutable FROM item;
};
struct sibling : public parent {
sibling(TO const& t): item(t){}
mutable TO item;
};
parent* p = new child(from);
return static_cast< sibling const& >(*p).item;
};
Как уже указывали другие: С++ не предназначен для использования таким образом. Почему? C++ основан на идее типа, описывающего статические аспекты поведения, и поэтому им нельзя манипулировать. Это позволяет выполнять множество проверок во время компиляции, поскольку все о типе известно заранее.
шаблон PIMPl
Однако существует хорошо известный, переносимый и общепринятый шаблон проектирования, который может обеспечить желаемое поведение: указатель на реализацию ("PImpl").
- иметь класс интерфейса в качестве внешнего интерфейса; это раскрывает функции, которые должны вызывать ваши клиенты
- этот класс содержит закрытый указатель на класс реализации и перенаправляет каждый вызов из внешних функций соответствующим функциям в классе реализации.
Для вашего желаемого варианта использования вы можете предоставить дополнительный API, который заставляет интерфейсный объект переключать свой внутренний объект реализации, что приводит к динамическому изменению поведения.
Так как указатель "PImpl" является приватным, вы можете использовать любые хитрые приемы, и вы можете сделать это полностью внутри реализации внешнего класса (в файле *.cpp). Вы можете поместить их во встроенный буфер (с новым размещением), или вы можете хранить их как синглтоны в отдельном диспетчере, или вы можете использовать пул экземпляров и схему распределения блоков — все, что вам нужно для достижения конкретных целей производительности.
В самой простой форме PImpl — это просто интеллектуальный указатель с единственным владельцем, а объекты реализации хранятся в куче.
class Interface
{
public:
virtual ~Interface() {}
virtual void doIt() =0;
};
class ImplA : public Interface
{
void doIt() override { /* impl A */ }
};
class ImplB : public Interface
{
void doIt() override { /* impl B */ }
};
class FrontEnd
{
std::unique_ptr<Interface> pimpl_;
public:
FrontEnd()
: pimpl_{new ImplA()}
{ }
void doIt() { pimpl_->doIt(); }
void switchB() { pimpl_.reset(new ImplB();) }
};