Почему std::polymorphic не поддерживает эффективное перемещающее конструирование?
Исследуйте технические причины, по которым std::polymorphic<Base> не может быть эффективно перемещенно сконструирован из std::polymorphic<Derived>. Понимайте компромиссы в дизайне в C++26.
Почему я не могу эффективно выполнить перемещающее конструирование std::polymorphic<Base> из std::polymorphic<Derived>?
Как я понимаю, std::polymorphic<T> и std::indirect<T> — это типы-обертки, распространяющие const, для владеющего указателя на объект типа T, где цель указателя копируется вместе с самим указателем. Упрощая, каждый из них можно рассматривать как “уникальный указатель std::unique_ptr<T>, который можно копировать”, при основном различии между ними заключается в том, что std::indirect<T> всегда будет указывать на объект точного типа T, в то время как std::polymorphic<T> может потенциально содержать указатель на объект производного типа.
Но есть существенное отличие от std::unique_ptr:
struct Base {};
struct Derived : Base {};
std::unique_ptr<Base> p = std::make_unique<Derived>(); // OK
std::indirect<Base> id = std::indirect<Derived>(); // ошибка
std::polymorphic<Base> pm = std::polymorphic<Derived>(); // ошибка
Я понимаю, почему это не работает для std::indirect — в конце концов, такое преобразование, если оно вообще разрешено, требует нарезки (slicing), что обычно нежелательно, поэтому лучше, чтобы оно не происходило случайно. Но меня гораздо больше озадачивает, что это не работает и для std::polymorphic.
Я видел в P0206R6, что на каком-то этапе, когда это все еще предлагалось как std::polymorphic_value, такое преобразование даже должно было быть неявным, но позже его сделали явным, а к моменту слияния предложения с P1950R2 в совместный документ P3019 преобразования полностью исчезли. Ни одного такого конструктора нет в [polymorphic.ctor] в последнем черновике.
Похоже, что std::polymorphic<T> можно перемещенно конструировать или перемещенно присваивать только из другого std::polymorphic<T>, и лучшее, что можно сделать для конструирования одного из цели std::polymorphic<U> pmu (где U наследуется от T) — это через std::move(*pmu), что менее эффективно, так как создает новый объект и оставляет цель источника в состоянии после перемещения, вместо того чтобы напрямую передать выделение памяти из источника в назначение и оставить источник не имеющим значения, как это происходит при перемещениях объектов одного и того же целевого типа.
Почему эта возможность полностью исчезла?
Невозможность эффективного перемещающего конструирования std::polymorphic<Base> из std::polymorphic<Derived> обусловлена несколькими фундаментальными решениями в проектировании стандартной библиотеки C++, которые придают приоритет безопасности типов, производительности и четкой семантики вместо синтаксического удобства.
Команда разработчиков std::polymorphic (ранее известного как std::polymorphic_value) сознательно удалила конструкторы преобразования, которые присутствовали в более ранних предложениях, таких как P0206R6. Этот выбор отражает тщательный баланс между полиморфным поведением и строгой семантикой владения, требуемой современным управлением памятью в C++.
Содержание
- Понимание пространства проектирования
- Техническое обоснование отсутствия конструкторов
- Последствия для производительности
- Альтернативные подходы
- Сравнение с другими типами
- Перспективы развития
Понимание пространства проектирования
std::polymorphic<T> и std::indirect<T> представляют значительную эволюцию в семантике указателей C++. В отличие от std::unique_ptr<T>, который обеспечивает строгое владение и семантику перемещения без возможности копирования, эти типы-обертки предоставляют копируемые полиморфные контейнеры.
Фундаментальное различие между этими типами и традиционными указателями заключается в модели владения. Как демонстрирует реализация CPP Rendering, полиморфные типы используют техники стирания типов для управления владением, сохраняя при этом полиморфное поведение.
Команда разработчиков столкнулась с критическим решением: разрешать ли неявные преобразования между полиморфными типами с разными базовыми классами. Ранние версии предложения (P0206R6) включали такие преобразования, но они были subsequently удалены, когда предложение объединилось с P1950R2 в P3019.
Техническое обоснование отсутствия конструкторов
Безопасность типов и предотвращение нарезки
Основная проблема, препятствующая неявным конструкторам преобразования, связана с безопасностью типов. Хотя std::polymorphic<Base> теоретически может содержать объект Derived, разрешение неявного конструирования создает несколько проблем:
-
Путаница типов: Преобразование может сделать неясным, содержит ли результирующий
std::polymorphic<Base>на самом деле объектBaseили производного объекта, потенциально приводя к тонким ошибкам. -
Семантика нарезки: В отличие от
std::unique_ptr, который сохраняет динамический тип, преобразование может подразумевать нарезку типов, которую пользователи не ожидают. -
Безопасность при исключениях: Как отмечено в обсуждении на Reddit о C++26
std::indirect, разрешение таких преобразований может усложнить гарантии обработки исключений.
Сложности управления памятью
Команда разработчиков признала, что полиморфные типы вводят уникальные проблемы управления памятью. При преобразовании между std::polymorphic<Derived> и std::polymorphic<Base> возникают несколько вопросов:
-
Отслеживание выделения: Каждый полиморфный оберток поддерживает собственные метаданные выделения. Преобразование между типами потребовало бы сложного преобразования метаданных.
-
Владение ресурсами: Преобразование потребовало бы передачи владения при сохранении правильной семантики очистки через разные иерархии типов.
-
Последствия для производительности: Как объясняется в статье PVS Studio о полиморфных аллокаторах, полиморфные конструкторы могут вести себя иначе, чем ожидают разработчики, потенциально приводя к неожиданностям в производительности.
Согласованность интерфейсов
Команда стандартной библиотеки придает приоритет согласованным интерфейсам между похожими типами. Удалив конструкторы преобразования, они обеспечивают, чтобы все полиморфные типы следовали одним и тем же правилам конструирования, снижая когнитивную нагрузку на разработчиков.
Последствия для производительности
Отсутствие эффективных конструкторов перемещения между разными полиморфными типами имеет заметные последствия для производительности. Когда разработчикам необходимо преобразовывать между std::polymorphic<Derived> и std::polymorphic<Base>, они вынуждены прибегать к менее эффективным подходам:
// Неэффективный подход, создающий новые объекты
std::polymorphic<Base> pm_base = std::move(*pm_derived);
Этот подход страдает от нескольких проблем производительности:
-
Воссоздание объекта: Целевой объект должен быть воссоздан, а не просто передано владение существующим выделением.
-
Перегрузка памяти: Происходят дополнительные выделения и освобождения памяти в процессе преобразования.
-
Перегрузка CPU: Операция копирования или перемещения требует выполнения конструкторов копирования/перемещения объекта, что может быть дорогостоящим для сложных объектов.
Влияние на производительность особенно проблематично в сценариях с частыми преобразованиями или большими объектами, где возможность прямой передачи владения была бы крайне полезной.
Альтернативные подходы
Хотя стандартная библиотека не предоставляет эффективных конструкторов преобразования, разработчики могут обойти эти ограничения с помощью нескольких подходов:
Явное конструирование
Наиболее прямой подход involves явное конструирование:
std::polymorphic<Base> pm_base(std::move(pm_derived));
Однако это все еще страдает от тех же проблем производительности, что и косвенный подход.
Вспомогательные функции
Разработчики могут создавать вспомогательные функции, выполняющие эффективные преобразования:
template<typename Base, typename Derived>
std::polymorphic<Base> polymorphic_cast(std::polymorphic<Derived>&& source) {
return std::polymorphic<Base>(std::move(source));
}
Пользовательские реализации
Для критически важного по производительности кода разработчики могут реализовать собственные полиморфные обертки, включающие желаемую семантику преобразования:
template<typename T>
class efficient_polymorphic {
// Реализация, поддерживающая эффективное преобразование
};
Сравнение с другими типами
std::unique_ptr
Поведение std::unique_ptr<Base> резко контрастирует с std::polymorphic<Base>:
std::unique_ptr<Base> p = std::make_unique<Derived>(); // OK
std::polymorphic<Base> pm = std::polymorphic<Derived>(); // ошибка
Это различие обусловлено принципиально разными целями проектирования:
std::unique_ptrподчеркивает строгое владение и семантику перемещенияstd::polymorphicподчеркивает возможность копирования и полиморфное поведение
std::shared_ptr
Аналогично std::unique_ptr, std::shared_ptr<Base> может быть сконструирован из std::shared_ptr<Derived> без проблем, демонстрируя, что ограничение специфично для дизайна std::polymorphic.
std::any и std::variant
Эти контейнеры со стиранием типов обрабатывают преобразования по-разному, поддерживая более гибкое преобразование типов за счет разных характеристик производительности.
Перспективы развития
Проектирование std::polymorphic продолжает развиваться. Удаление конструкторов преобразования в P3019 отражает консервативный подход к безопасности типов и производительности. Будущие версии могут:
-
Добавить явные конструкторы преобразования: Предоставить безопасные, явные методы преобразования, сохраняющие производительность.
-
Оптимизировать текущий подход: Улучшить эффективность существующих шаблонов преобразования для снижения штрафов за производительность.
-
Ввести новые вспомогательные функции: Добавить функции стандартной библиотеки для эффективной обработки распространенных сценариев преобразования.
-
Улучшить типовые характеристики: Предоставить лучшую компилируемую проверку типов для допустимых преобразований между полиморфными типами.
Продолжающиеся дискуссии вокруг этих типов, как видно в обсуждении C++26 std::indirect и std::polymorphic, показывают, что команда стандартной библиотеки остается приверженной поиску оптимального баланса между гибкостью, безопасностью и производительностью.
Заключение
Невозможность эффективно перемещать-конструировать std::polymorphic<Base> из std::polymorphic<Derived> представляет собой сознательный выбор проектирования, который придает приоритет безопасности типов и согласованным интерфейсам вместо синтаксического удобства. Команда стандартной библиотеки удалила конструкторы преобразования из спецификации для предотвращения потенциальной путаницы, обеспечения правильного управления памятью через иерархии типов и поддержания согласованного поведения между похожими типами.
Хотя этот подход имеет последствия для производительности, он соответствует более широкой философии проектирования C++, предпочитая явные, безопасные операции неявным, потенциально опасным. Разработчики, работающие с этими типами, должны быть осведомлены об ограничениях и соответственно планировать свои проекты, возможно используя вспомогательные функции или пользовательские реализации для критически важных по производительности сценариев преобразования.
По мере продолжения эволюции стандарта C++ мы можем увидеть улучшения этого дизайна, которые лучше сбалансируют конкурирующие требования безопасности, производительности и удобства разработчика.
Источники
- Обсуждение C++26
std::indirectиstd::polymorphicна Reddit - Стирание типов в C++: Реализация
std::polymorphic- CPP Rendering - Как frivolous использование полиморфных аллокаторов может испортить вашу жизнь - PVS Studio
- Конструкторы перемещения в C++ - GeeksforGeeks
- Что такое конструктор преобразования в C++? - GeeksforGeeks