Я стремлюсь отделить интерфейс от реализации. Это в первую очередь для защиты кода, использующего библиотеку, от изменений в реализации указанной библиотеки, хотя, конечно, приветствуются сокращенные времена компиляции.
Стандартное решение для этого - указатель на идиому реализации, скорее всего, будет реализован с помощью unique_ptr и тщательного определения деструктора класса вне строки с реализацией.
Неизбежно это вызывает озабоченность по поводу распределения кучи. Я знаком с "заставить его работать, а затем быстро", "профиль затем оптимизировать" и такую мудрость. Есть также статьи в Интернете, например. gotw, которые объявляют очевидным обходным путем хрупким и не переносным. У меня есть библиотека, которая в настоящее время не содержит никаких распределений кучи - и я хотел бы сохранить ее таким образом, поэтому пусть у вас есть какой-то код.
#ifndef PIMPL_HPP
#define PIMPL_HPP
#include <cstddef>
namespace detail
{
// Keeping these up to date is unfortunate
// More hassle when supporting various platforms
// with different ideas about these values.
const std::size_t capacity = 24;
const std::size_t alignment = 8;
}
class example final
{
public:
// Constructors
example();
example(int);
// Some methods
void first_method(int);
int second_method();
// Set of standard operations
~example();
example(const example &);
example &operator=(const example &);
example(example &&);
example &operator=(example &&);
// No public state available (it all in the implementation)
private:
// No private functions (they're also in the implementation)
unsigned char state alignas(detail::alignment)[detail::capacity];
};
#endif
Это не выглядит слишком плохо для меня. Выравнивание и размер можно статически утверждать в ходе реализации. Я могу выбирать между переоценкой обоих (неэффективными) или перекомпиляцией всего, если они меняются (утомительно), но ни один из вариантов не страшен.
Я не уверен, что этот хакер будет работать в присутствии наследования, но поскольку мне не очень нравится наследование в интерфейсах, я не против слишком много.
Если смело предположить, что я правильно написал реализацию (я добавлю ее к этому сообщению, но это непроверенное доказательство концепции в этот момент, так что не данный), и размер и выравнивание больше, чем или равна реализации, то реализует ли реализация реализации кода, или undefined, поведение?
#include "pimpl.hpp"
#include <cassert>
#include <vector>
// Usually a class that has behaviour we care about
// In this example, it arbitrary
class example_impl
{
public:
example_impl(int x = 0) { insert(x); }
void insert(int x) { local_state.push_back(3 * x); }
int retrieve() { return local_state.back(); }
private:
// Potentially exotic local state
// For example, maybe we don't want std::vector in the header
std::vector<int> local_state;
};
static_assert(sizeof(example_impl) == detail::capacity,
"example capacity has diverged");
static_assert(alignof(example_impl) == detail::alignment,
"example alignment has diverged");
// Forwarding methods - free to vary the names relative to the api
void example::first_method(int x)
{
example_impl& impl = *(reinterpret_cast<example_impl*>(&(this->state)));
impl.insert(x);
}
int example::second_method()
{
example_impl& impl = *(reinterpret_cast<example_impl*>(&(this->state)));
return impl.retrieve();
}
// A whole lot of boilerplate forwarding the standard operations
// This is (believe it or not...) written for clarity, so none call each other
example::example() { new (&state) example_impl{}; }
example::example(int x) { new (&state) example_impl{x}; }
example::~example()
{
(reinterpret_cast<example_impl*>(&state))->~example_impl();
}
example::example(const example& other)
{
const example_impl& impl =
*(reinterpret_cast<const example_impl*>(&(other.state)));
new (&state) example_impl(impl);
}
example& example::operator=(const example& other)
{
const example_impl& impl =
*(reinterpret_cast<const example_impl*>(&(other.state)));
if (&other != this)
{
(reinterpret_cast<example_impl*>(&(this->state)))->~example_impl();
new (&state) example_impl(impl);
}
return *this;
}
example::example(example&& other)
{
example_impl& impl = *(reinterpret_cast<example_impl*>(&(other.state)));
new (&state) example_impl(std::move(impl));
}
example& example::operator=(example&& other)
{
example_impl& impl = *(reinterpret_cast<example_impl*>(&(other.state)));
assert(this != &other); // could be persuaded to use an if() here
(reinterpret_cast<example_impl*>(&(this->state)))->~example_impl();
new (&state) example_impl(std::move(impl));
return *this;
}
#if 0 // Clearer assignment functions due to MikeMB
example &example::operator=(const example &other)
{
*(reinterpret_cast<example_impl *>(&(this->state))) =
*(reinterpret_cast<const example_impl *>(&(other.state)));
return *this;
}
example &example::operator=(example &&other)
{
*(reinterpret_cast<example_impl *>(&(this->state))) =
std::move(*(reinterpret_cast<example_impl *>(&(other.state))));
return *this;
}
#endif
int main()
{
example an_example;
example another_example{3};
example copied(an_example);
example moved(std::move(another_example));
return 0;
}
Я знаю это довольно ужасно. Я не против использования генераторов кода, хотя это не то, что мне придется печатать повторно.
Чтобы сформулировать суть этого чрезмерно длинного вопроса в явном виде, следующие условия достаточны, чтобы избежать UB | IDB?
- Размер совпадения совпадений размера экземпляра impl
- Выравнивание совпадений совпадений с экземпляром impl
- Все пять стандартных операций, реализованных в терминах impl
- Размещение нового, используемого правильно
- Явные вызовы деструктора, используемые правильно
Если это так, я напишу достаточно тестов для Valgrind, чтобы очистить несколько ошибок в демо. Спасибо всем, кто заберет это!
edit: Можно надавить много шаблона в базовый класс. Там репо на моем github называется "pimpl", который исследует это. Я не думаю, что есть хороший способ неявно создавать произвольные переадресованные конструкторы, поэтому есть еще больше ввода текста, чем хотелось бы.