Сделать взаимозаменяемые типы классов только с помощью приведения указателя, без необходимости выделять какие-либо новые объекты?

ОБНОВЛЕНИЕ: я ценю предложения "не хочу этого, хочу этого". Они полезны, особенно если они представлены в контексте мотивирующего сценария. Тем не менее... независимо от добра / зла, мне стало любопытно найти твердое "да, это можно сделать легально в 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,

Другие вопросы по тегам