Новый (ptr) T() == static_cast<T*>(ptr)?

Я хочу реализовать что-то вроде черты ржавчины dyn (я знаю, что это не работает для множественного наследования)

template<template<typename> typename Trait>
class Dyn
{
    struct _SizeCaler:Trait<void>{ void* _p;};
    char _buffer[sizeof(_SizeCaler)];
public:
    template<typename T>
    Dyn(T* value){
        static_assert(std::is_base_of_v<Trait<void>,Trait<T>>
            ,"error Trait T,is not derive from trait<void>"
        );

        static_assert(sizeof(_buffer) >= sizeof(Trait<T>)
            ,"different vtable imple cause insufficient cache"
        );

        new (_buffer)Trait<T>{value}; 
    }
    Trait<void>* operator->(){
        return static_cast<Trait<void>*>(reinterpret_cast<void*>(_buffer));
    }
};

template<template<typename> typename Trait,typename T>
struct DynBase:Trait<void>
{
protected:
    T* self;
public:
    DynBase(T* value):self(value){}
};

struct Point{
    double x,y;
};

struct Rect{
    Point m_leftDown,m_rightUp;
    Rect(Point p1,Point p2){
        m_leftDown = Point{std::min(p1.x,p2.x),std::min(p1.y,p2.y)}; 
        m_rightUp = Point{std::max(p1.x,p2.x),std::max(p1.y,p2.y)}; 
    }
};

template<typename = void>
struct Shape;

template<>
struct Shape<void>
{
    virtual double area() = 0;
};

template<>
struct Shape<Rect> final
    :DynBase<Shape,Rect>
{
    using DynBase<Shape,Rect>::DynBase;
    double area() override{
        return (self->m_rightUp.x - self->m_leftDown.x )
            * (self->m_rightUp.y - self->m_leftDown.y);
    }
};

void printShape(Dyn<Shape> ptr)
{
    std::cout << ptr->area();
}

int main()
{
    Rect r{{1,2},{3,4}};
    printShape(&r);
}

но я обнаружил, что стандарт С++ не может вывести «new (ptr) T() == static_cast<T*>(ptr)»? И наоборот, «static_cast<T*>(ptr) == new (ptr) T()» не может доказать

Trait<void>* operator->(){ return static_cast<Trait<void>*>(reinterpret_cast<void*>(_buffer)); }

Другие попытки

класс abstruct не может быть членом объединения (почему?), и я не могу использовать новое размещение для вычисления смещения во время компиляции, потому что Trait является абстрактным классом.

Так определяет ли стандарт действительность static_cast<T*>(ptr) == new (ptr) T()?

char _buffer[sizeof(_SizeCaler)]; должен быть выровнен с Trait<T>
KamilCuk 27.10.2022 19:23

Это хороший вопрос. Равенство часто предполагается, но мне любопытно, гарантируется ли оно. Рассмотрите возможность добавления тега language-lawyer для стандартных ответов.

François Andrieux 27.10.2022 19:25

Шаблон Dyn не может получить информацию о T, ему нужно набрать стереть T без потери производительности.

余国良 27.10.2022 19:26

Вы не можете получить стирание типа без потери производительности.

NathanOliver 27.10.2022 19:32

Абстрактный класс не может быть членом любого другого типа. Про профсоюзы ничего особенного.

Ben Voigt 27.10.2022 19:35

Я должен сказать, что нет дополнительных штрафов за производительность, а не штраф за производительность, что является моей проблемой выражения.

余国良 27.10.2022 19:37

@FrançoisAndrieux: правила для operator new() запрещают накладные расходы (что подразумевает, что указатель возвращается с нулевым смещением), в то время как operator new[] допускают накладные расходы, которые почти всегда находятся в начале распределения, поэтому требуется смещение. Это создает некоторую загадку для размещения operator new[]. Я думаю, что это было признано дефектом и, возможно, было исправлено.

Ben Voigt 27.10.2022 19:37

@BenVoigt Меня беспокоили безопасные указатели. Но я изучил это, и в С++ 23 этого больше не будет, и ни один основной компилятор не заботится об этом. Так что это не совсем проблема.

François Andrieux 27.10.2022 19:58
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
6
8
113
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

Ответ принят как подходящий

Выражения new (ptr) T() и static_cast<T*>(ptr), где ptr имеет тип void*, вернут один и тот же адрес, ptr (пока T является скалярным типом — типы массивов могут иметь накладные расходы при динамическом распределении)

Однако семантика совсем другая.

  • В new (ptr) T() по указанному адресу создается новый объект типа T. Любое предыдущее значение теряется.

  • В static_cast<T*>(ptr) по указанному адресу уже должен быть объект типа T, иначе вы настраиваете себя на нарушение строгого правила алиасинга.


Если бы ptr имел тип, отличный от void*, он должен был бы быть связан с T путем наследования или тривиальных корректировок (таких как изменение подписи или добавление const или volatile), а к адресу могло бы быть применено смещение в результате множественного наследования. Но этого никогда не произойдет в коде этого вопроса.

@FrançoisAndrieux: я сказал, что «выражения вернут один и тот же адрес»

Ben Voigt 27.10.2022 19:39

Возможность служебных данных массива была удалена для отчета о дефекте без выделения new через CWG 2382.

user17732522 27.10.2022 19:39

emm, я знаю, что в стандарте указано, что оператор void* new(size_t, void* ptr) вернет ptr без изменений, но new (ptr) T() вернет указатель T*, а не void*. Это может означать, что они разные ?

余国良 27.10.2022 19:50

@余国良 void* operator new(size_t ,void* ptr) — это функция распределения. new (ptr) T() — новое выражение. Новое выражение вызывает одну функцию распределения для получения хранилища, затем вызывает запуск конструктора объекта с использованием этого хранилища для объекта, а затем возвращает указатель на этот объект. Новое выражение размещения использует эту нераспределяющую функцию распределения, чтобы просто вернуть указатель.

François Andrieux 27.10.2022 19:55

@FrançoisAndrieux, поскольку это разные выражения, ограничение «без изменений указателя» для первого не может быть передано второму?

余国良 27.10.2022 19:58

@ 余国良 Я считаю, что гарантия является транзитивной, я не думаю, что объект можно поместить со смещением к этому указателю. В противном случае становится невозможно узнать, насколько большим должно быть хранилище, которое вам нужно выделить для размещения new. Это была проблема, упомянутая в этом ответе, связанная с использованием нового размещения с массивами. Но я полностью уверен.

François Andrieux 27.10.2022 20:01

В терминах чистой реализации это можно сделать. Поместите виртуальную таблицу в позицию ptr и верните позицию после виртуальной таблицы. Нет смещения ни для static_cast, ни для void*, ни для T*. Этот шаблон реализации, похоже, соответствует всем стандартам С++? Но у моей реализации Dyn будут проблемы

余国良 27.10.2022 20:05

@余国良 Trait<T>* должен иметь тот же адрес, что и _buffer. Это требуется стандартом, как объясняется в этом ответе. Но подобъект Trait<void>* обычно может быть смещен от этого именно из-за места, необходимого для указателя виртуальной таблицы производного объекта. Указатель, возвращаемый new, не может указывать дальше указателя vtable, потому что адрес объекта должен относиться к первому байту памяти, занимаемому объектом.

user17732522 27.10.2022 20:17

@user17732522 user17732522 Можете ли вы дать мне ссылку на это описание? («Трейт<T>* должен иметь тот же адрес, что и _buffe»)

余国良 27.10.2022 20:20

@余国良 Это нигде явно не прописано. Это следствие того, что новое выражение требуется для получения только sizeof(Trait<T>) памяти из operator new, поэтому нет возможности смещения для размещения объекта, и, как я уже сказал, адрес объекта всегда соответствует первому байту занимаемой им памяти. . Но в основном eel.is/c++draft/expr.new#16.sentence-2 является важной частью.

user17732522 27.10.2022 20:22

@user17732522 user17732522 А это не так. Если в стандарте это не указано, размещение виртуальной таблицы перед указателем является возможной реализацией, когда свойство является типом, содержащим виртуальные функции. Это не влияет на присваивание sizeof, но требует выполнения смещения, когда указатель типажа возвращается после нового значения.

余国良 27.10.2022 20:26

@ 余国良 Нет, это недопустимая реализация. eel.is/c++draft/basic#intro.object-9.sentence-1 требует, чтобы адрес объекта был адресом первого занятого им байта. Память, используемая для указателя vtable, является частью объекта.

user17732522 27.10.2022 20:29

Давайте продолжим обсуждение в чате.

余国良 27.10.2022 20:31

Является ли new (ptr) T() == static_cast<T*>(ptr) правдой, это не ваша проблема.

Фактическая проблема заключается в том, что класс Trait<T>, который вы создаете, не имеет стандартной компоновки, и, как следствие, Trait<void> подобъект Trait<T> объекта не является взаимопреобразуемым указателем с Trait<T> объектом, и при этом он не должен быть расположен по тому же адресу. Как следствие, вы не можете привести от _buffer к Trait<void>*, не зная T или не имея указателя на Trait<void> конкретно.

Другими словами, вам нужно сохранить дополнительный указатель Trait<void>* в классе, который вы инициализируете из результата new (_buffer)Trait<T>{value}; (через неявное преобразование), или, по крайней мере, смещение между Trait<T> и его подобъектом Trait<void>. (В последнем случае имейте в виду, что вам нужно добавить std::launder после reinterpret_cast.)

Кроме того, как упоминалось в комментариях, вам необходимо убедиться, что буфер правильно выровнен для объекта типа Trait<T>. Это требует добавления alignas к буферу, чтобы он подходил для всех T, которые вы хотите рассмотреть, а затем, вероятно, должен быть static_assert в выравнивании, аналогичном тому, что и для размера.


Кроме того, static_cast<Trait<void>*>(reinterpret_cast<void*>(_buffer)) эквивалентно reinterpret_cast<Trait<void>*>(_buffer). Нет необходимости в двойном броске.

Есть ли разница между двумя приведениями и одним приведением зависит от реализации? Если static_cast<T*> будет иметь смещение. Кроме того, предполагается, что эта реализация основана на одиночном наследовании

余国良 27.10.2022 20:15

@余国良 static_cast от Trait<void>* до Trait<T>* обычно имеет смещение, но приведение между void* и Trait<T>* (или Trait<void>* или любой другой тип указателя объекта в этом отношении) никогда не имеет.

user17732522 27.10.2022 20:20

Другой вопрос:

абстрактный класс не может быть членом союза (почему?)

потому что нет смысла иметь абстрактный класс в качестве члена союза. Чтобы использовать член объединения, вам необходимо создать экземпляр объекта типа члена объединения для хранения в объединении. Но вы не можете создать экземпляр объекта абстрактного класса, поэтому вы никогда не сможете использовать член объединения абстрактного класса для чего-либо.

но это обеспечит относительно безопасный переход от буфера к Trait<void>

余国良 27.10.2022 20:17

@余国良 Это не так. union нельзя использовать для стирания или преобразования типов в C++. Чтение члена union, который не был назначен последним, называется UB в C++.

François Andrieux 27.10.2022 20:32

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