Подтвердить что ты не робот

Без кучи. Неправильно или суеверия?

Я стремлюсь отделить интерфейс от реализации. Это в первую очередь для защиты кода, использующего библиотеку, от изменений в реализации указанной библиотеки, хотя, конечно, приветствуются сокращенные времена компиляции.

Стандартное решение для этого - указатель на идиому реализации, скорее всего, будет реализован с помощью 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", который исследует это. Я не думаю, что есть хороший способ неявно создавать произвольные переадресованные конструкторы, поэтому есть еще больше ввода текста, чем хотелось бы.

4b9b3361

Ответ 1

Да, это абсолютно безопасный и портативный код.

Однако нет необходимости использовать новое и явное уничтожение места размещения в ваших операторах присваивания. Кроме того, что это исключение, безопасное и более эффективное, я бы также сказал, что он намного проще использовать оператор присваивания example_impl:

//wrapping the casts
const example_impl& castToImpl(const unsigned char* mem) { return *reinterpret_cast<const example_impl*>(mem);  }
      example_impl& castToImpl(      unsigned char* mem) { return *reinterpret_cast<      example_impl*>(mem);  }


example& example::operator=(const example& other)
{
    castToImpl(this->state) = castToImpl(other.state);
    return *this;
}

example& example::operator=(example&& other)
{
    castToImpl(this->state) = std::move(castToImpl(other.state));
    return *this;
}

Лично я также использовал std::aligned_storage вместо массива char, выровненного вручную, но я думаю, что это вопрос вкуса.