Сделать взаимозаменяемые типы классов только с помощью приведения указателя, без необходимости выделять какие-либо новые объекты?
ОБНОВЛЕНИЕ: я ценю предложения "не хочу этого, хочу этого". Они полезны, особенно если они представлены в контексте мотивирующего сценария. Тем не менее... независимо от добра / зла, мне стало любопытно найти твердое "да, это можно сделать легально в C++11" против "нет, что-то подобное сделать невозможно".
Я хочу "псевдоним" указателя объекта в качестве другого типа, с единственной целью добавления некоторых вспомогательных методов. Псевдоним не может добавлять члены данных в базовый класс (на самом деле, чем больше я могу предотвратить это, тем лучше!) Все псевдонимы одинаково применимы к любому объекту этого типа... это просто полезно, если система типов может намекнуть, какая псевдоним, вероятно, является наиболее подходящим.
Не должно быть никакой информации о каком-либо конкретном псевдониме, который когда-либо кодируется в базовом объекте. Следовательно, я чувствую, что вы должны быть в состоянии "обмануть" систему типов и просто позволить ей быть аннотацией... проверенной во время компиляции, но в конечном счете не имеющей отношения к приведению во время выполнения. Что-то вроде этого:
Node<AccessorFoo>* fooPtr = Node<AccessorFoo>::createViaFactory();
Node<AccessorBar>* barPtr = reinterpret_cast< Node<AccessorBar>* >(fooPtr);
Под капотом заводской метод на самом деле делает NodeBase
класс, а затем с помощью аналогичного reinterpret_cast
вернуть его как Node<AccessorFoo>*
,
Самый простой способ избежать этого - создать легкие классы, которые обертывают узлы и передаются по значению. Таким образом, вам не нужно приведение, только классы Accessor, которые принимают дескриптор узла для переноса в своем конструкторе:
AccessorFoo foo (NodeBase::createViaFactory());
AccessorBar bar (foo.getNode());
Но если мне не придется платить за все это, я не хочу. Это может включать, например, создание специального типа средства доступа для каждого вида обернутых указателей (AccessorFooShared, AccessorFooUnique, AccessorFooWeak и т. Д.). Предпочтительно, чтобы эти типизированные указатели были псевдонимами для одного идентификатора объекта, основанного на указателе, и являлись хорошая ортогональность
Итак, вернемся к этому первоначальному вопросу:
Node<AccessorFoo>* fooPtr = Node<AccessorFoo>::createViaFactory();
Node<AccessorBar>* barPtr = reinterpret_cast< Node<AccessorBar>* >(fooPtr);
Похоже, что есть какой-то способ сделать это, который может быть уродливым, но не "нарушать правила". В соответствии с ISO14882: 2011 (e) 5.2.10-7:
Указатель на объект может быть явно преобразован в указатель на объект другого типа.70 Когда значение v типа "указатель на T1" преобразуется в тип "указатель на cv T2", результатом является static_cast(static_cast(v)) если и T1, и T2 являются типами стандартной компоновки (3.9) и требования к выравниванию T2 не являются более строгими, чем требования для T1, или если какой-либо тип является недействительным. Преобразование значения типа "указатель на T1" в тип "указатель на T2" (где T1 и T2 являются типами объектов и где требования к выравниванию T2 не более строгие, чем требования к T1) и обратно к его исходному типу дает исходный значение указателя Результат любого другого такого преобразования указателя не определен.
Детализируя определение "стандартного макета класса", мы находим:
- не имеет нестатических членов-данных типа non-standard-layout-class (или массива таких типов) или ссылки, и
- не имеет виртуальных функций (10.3) и виртуальных базовых классов (10.1), и
- имеет одинаковый контроль доступа (пункт 11) для всех нестатических элементов данных, и
- не имеет базовых классов нестандартной компоновки и
- либо не имеет нестатического члена данных в самом производном классе и не более одного базового класса с нестатическими членами данных, или не имеет базовых классов с нестатическими членами данных, и
- не имеет базовых классов того же типа, что и первый нестатический член данных.
Похоже, что работа с чем-то вроде этого немного связывает мои руки без виртуальных методов в средствах доступа или узле. Тем не менее, C++ 11, очевидно, имеет std::is_standard_layout
держать вещи проверенными.
Можно ли это сделать безопасно? Кажется, работает в gcc-4.7, но я хотел бы убедиться, что я не вызываю неопределенное поведение.
4 ответа
Термин " Accessor" - это полная раздача: вы ищете Proxy.
Нет никаких причин, чтобы Прокси не передавался по значению.
// Let us imagine that NodeBase is now called Node, since there is no inheritance
class AccessorFoo {
public:
AccessorFoo(Node& n): node(n) {}
int bar() const { return node->bar; }
private:
std::reference_wrapper<Node> node;
};
И тогда вы можете свободно конвертировать из одного средства доступа в другое... хотя это пахнет. Как правило, самой целью наличия средства доступа является ограничение доступа таким образом, что приведение невольного воли к другому устройству доступа является плохим. Однако можно поддержать приведение к более узкому доступу.
Я считаю, что строгие правила наложения запрещают то, что вы пытаетесь сделать.
Чтобы уточнить: строгое псевдоним не имеет ничего общего с совместимостью макетов, типами POD или чем-то еще. Это связано с оптимизацией. И с тем, что язык явно запрещает вам делать.
Эта статья довольно хорошо подытоживает: http://dbp-consulting.com/StrictAliasing.pdf
Если я вас правильно понимаю, у вас есть:
NodeBase
класс с состоянием и истинная рабочая лошадка системы;- набор лиц без гражданства
Accessor
типы, которые предоставляют интерфейс дляNodeBase
; а также Node<AccessorT>
класс, который обёртывает аксессор, предположительно предоставляя вспомогательные функции.
Я предполагаю последний бит, потому что если у вас нет типа-обертки, который делает удобные вещи, то нет никаких причин не делать Accessor
наберите свой верхний уровень, как вы предложили: pass AccessorFoo
а также AccessorBar
вокруг по значению. Тот факт, что они не являются одним и тем же объектом, является спорным; если вы думаете о них как о указателях, то они заметят, что &foo != &bar
не более интересно, чем иметь NodeBase* p1 = new NodeBase; NodeBase* p2 = p1;
и отмечая, что, конечно же, &p1 != &p2
,
Если вам действительно нужна обертка Node<AccessorT>
и хочу сделать это стандартным макетом, тогда я бы предложил вам использовать безгражданство вашего Accessor
типы в ваших интересах. Если они являются просто контейнером функциональности без сохранения состояния (которым они должны быть; зачем еще вы могли бы свободно использовать их?), Тогда вы можете сделать что-то вроде этого:
struct AccessorFoo {
int getValue(NodeBase* n) { return n->getValueFoo(); }
};
struct AccessorBar {
int getValue(NodeBase* n) { return n->getValueBar(); }
};
template <typename AccessorT>
class Node {
NodeBase* m_node;
public:
int getValue() {
AccessorT accessor;
return accessor.getValue(m_node);
}
};
В этом случае вы можете добавить шаблонный оператор преобразования:
template <typename OtherT>
operator Node<OtherT>() {
return Node<OtherT>(m_node);
}
И теперь у вас есть прямое преобразование значений между любыми Node<AccessorT>
типа тебе нравится.
Если вы пойдете немного дальше, вы сделаете все методы Accessor
Типы статические, и прийти к шаблону черт.
Между прочим, приведенный вами раздел стандарта C++ касается поведения reinterpret_cast<T*>(p)
в случае, когда и исходный тип, и конечный тип являются указателями на объекты стандартного макета, и в этом случае стандарт гарантирует, что вы получите тот же указатель, который вы получите при приведении к void*
а затем до окончательного типа. Вы по-прежнему не можете использовать объект как любой другой тип, кроме того, для которого он был создан, без вызова неопределенного поведения.
struct myPOD {
int data1;
// ...
};
struct myPOD_extended1 : myPOD {
int helper() { (*(myPOD*)this)->data1 = 6; }; // type myPOD referenced here
};
struct myPOD_extended2 : myPOD {
int helper() { data1 = 7; }; // no syntactic ref to myPOD
};
struct myPOD_extended3 : myPOD {
int helper() { (*(myPOD*)this)->data1 = 8; }; // type myPOD referenced here
};
void doit(myPOD *it)
{
((myPOD_extended1*)it)->helper();
// ((myPOD_extended2*)it)->helper(); // uncomment -> program/compile undefined
((myPOD_extended3*)it)->helper();
}
int main(int,char**)
{
myPOD a; a.data1=5;
doit(&a);
std::cout<< a.data1 << '\n';
return 0;
}
Я считаю, что это гарантированно работает во всех соответствующих компиляторах C++ и должно вывести 8. Раскомментируйте отмеченную строку, и все ставки отключены.
Оптимизатор может обрезать поиск действительных ссылок с псевдонимами, проверяя список синтаксических типов, на которые фактически ссылается функция, по списку (в 3.10p10) синтаксических типов ссылок, которые необходимы для получения правильных результатов - и когда фактическая ("динамическая") ") тип объекта известен, этот список не включает доступ через ссылку на какой-либо производный тип. Таким образом, явные (и действительные) понижения this
в myPOD*
в helper()
ставит myPOD
в списке типов, синтаксически на которые есть ссылки, и оптимизатор должен рассматривать результирующие ссылки как потенциальные юридические ссылки на (другие имена, псевдонимы) объекта a
,