У меня есть class
в заголовочном файле, члены которого определены внутри класса pimpl. Идея состоит в том, что я использую этот метод (в основном std::aligned_storage_t
и указатель, но размер и выравнивание класса должны быть указаны при объявлении объекта) для размещения класса pimpl в стеке. Я хочу сделать код кросс-компилятором, чтобы угадывать не вариант, поэтому я определил 2 private
static constexpr
функции: impl_size
и impl_align
, которые определены в соответствующем исходном файле и в основном возвращают sizeof(pimpl)
и alignof(pimpl)
. Проблема в том, что я получаю следующую ошибку от MSVC (не проверено на других компиляторах):
expression must have a constant value -- constexpr function function "Type::impl_size" (declared at line 70) is not defined
строка 70 — это место, где impl_size
определено в заголовке.
MCVE:
#include <cstddef>
template <typename T1, std::size_t N1, std::size_t N2>
struct Pimpl;
class Type
{
private:
static constexpr std::size_t impl_size() noexcept; // line 70
static constexpr std::size_t impl_align() noexcept;
struct impl {};
Pimpl<impl, impl_size(), impl_align()> data; // error happens here
};
constexpr std::size_t Type::impl_size() noexcept
{
return sizeof(Type::impl);
}
constexpr std::size_t Type::impl_align() noexcept
{
return alignof(Type::impl);
}
Я добавил минималистичный пример. Если этого недостаточно, я рад «расширить» его до более полного.
Понятно, что компилятору необходимо знать значение impl_size()
во время компиляции, а для этого ему нужно видеть свое тело везде, где header.hpp
включен.
@TerensTare MCVE всегда должен быть завершен. Без исключений. Делайте это с самого начала, чтобы мы все не тратили время на то, чтобы снова и снова спрашивать об этом, как мы делаем по любому другому вопросу, который мы получаем ежедневно.
Это то, что он говорит: вы не определили свою функцию-член, когда объявили ее constexpr
.
Вы должны предоставить определение немедленно, встроенное в определение класса.
Есть пара проблем с вашим кодом, которые кажутся мне странными. Суть идиомы "PImpl" состоит в том, чтобы отделить интерфейс от его реализации, как правило, для упрощения компиляции при изменении реализации. Однако, определяя свой struct impl
внутри своего класса интерфейса и используя его в шаблоне в том же классе, вы, по сути, заставляете реализацию быть связанной с интерфейсом.
sizeof
и alignof
требуют полного типа, которого impl
не имеет (EDIT на момент написания, impl
был просто объявлен вперед), поэтому даже после исправления проблем с Type::impl_size()
и Type::impl_align()
вы столкнетесь с этой проблемой.
Немедленным и несколько поверхностным решением вашей проблемы было бы сделать impl
полным типом (и определить его в точке объявления), а также превратить impl_size()
и impl_align()
в функции inline static constexpr
, которые также определены на месте:
class type
{
// ...
private:
struct impl {
// struct definition, so that impl is a complete type
};
inline static constexpr impl_size() noexcept {
return sizeof(impl);
}
inline static constexpr impl_align() noexcept {
return alignof(impl);
}
Pimpl<impl, impl_size(), impl_align()> data;
};
Это все еще немного пахнет, потому что impl_size
и impl_align
— это бессмысленный шаблон, а ваш шаблон PImpl
может иметь всю ту же информацию непосредственно из своего первого параметра:
template<typename T>
class Pimpl {
static constexpr std::size_t Size = sizeof(T);
static constexpr std::size_t Alignment = alignof(T);
};
class type {
private:
struct impl {};
Pimpl<impl> data;
};
Это, конечно, также требует, чтобы impl
был полным типом. И это все равно потребуется, если struct impl является вложенным классом.
Похоже, вы пытаетесь сделать здесь какое-то стирание типов (или есть другая веская причина, по которой вам нужен размер и выравнивание impl
?), но непреднамеренно ввели много зависимостей от реализации и задействованных типов.
Я бы предложил следующее: предварительно объявите свой класс impl
в области пространства имен и просто используйте std::unique_ptr<impl>
для своей реализации. Затем impl
можно определить в файле реализации. Обратите внимание, что std::unique_ptr
не требует полного типа.
#include <iostream>
#include <memory>
// header.hpp
struct impl;
class type {
public:
type();
void doThing();
private:
std::unique_ptr<impl> m_pImpl;
};
// source.cpp
struct impl {
void doThingImpl() {
std::cout << "Did a thing";
}
};
type::type()
: m_pImpl(std::make_unique<impl>()) {
}
void type::doThing() {
m_pImpl->doThingImpl();
}
int main(){
auto t = type{};
t.doThing();
return 0;
}
Спасибо за Ваш ответ. Я пытаюсь избежать распределения кучи (это "забавный" проект), но, думаю, я буду использовать умные указатели.
Вы можете разместить новый объект impl
внутри блока хранилища, не распределенного в куче, непосредственно в классе type
, но вам потребуется жестко закодировать размер и выравнивание (или их верхние границы) объекта impl
для это работает (потому что вы не можете получить размер или выравнивание неполного типа). Это может быть забавно, если вам нужна экстремальная производительность, но я бы не стал поощрять это из соображений безопасности памяти и удобства обслуживания.
Это была идея, использованная с std::aligned_storage_t
, но я думаю, что тогда лучше использовать что-то еще, чем std::aligned_storage_t
. еще раз спасибо
На самом деле, если бы выделение кучи было моей самой большой проблемой, я бы просто реализовал все type
, используя обычные функции-члены, даже если это означает немного более длительную компиляцию здесь или там.
Меня не волнует время компиляции, я просто хочу не показывать членов Type
в заголовке. Это потому, что я хочу создать source.cpp как DLL, и я получаю предупреждения о создании экземпляров шаблонов от MSVC (один из элементов — это std::map
), поэтому я подумал, что, может быть, было бы лучше, если бы я просто «скрыл» эти элементы.
@TerensTare Мне стало любопытно, и я думаю, что у меня есть рабочая демонстрация относительно безопасного размещения нового PImpl: coliru.stacked-crooked.com/a/9237399182be0d1f Размер и выравнивание проверяются во время компиляции. Если проверки не пройдены, вы можете запросить их, просто определив макрос GIVE_ME_THE_SIZE
и используя выходные данные для исправления aligned_storage_t
Еще раз спасибо @alter igel. Проблема в том, что я хочу, чтобы это был кросс-компилятор, но, поскольку стандарт не накладывает никаких ограничений на размер std::
типов, я не могу просто закодировать их. Я предполагаю, что макросы, специфичные для компилятора, будут работать.
Верхние границы тоже должны работать, например impl_size
=> max_impl_size
и static_assert(sizeof(impl) <= max_impl_size);
Пожалуйста, отредактируйте свой пост и создайте минимальный воспроизводимый пример. Мы не можем исправить код, который не видим.