无堆的皮普尔.不正确或迷信? [英] Heap-free pimpl. Incorrect or superstition?

查看:97
本文介绍了无堆的皮普尔.不正确或迷信?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

这个问题可以追溯到C ++ 17之前.这些天,应该将std :: launder或等效参数添加到线路噪声中.我没有时间立即更新代码以使其匹配.

This question dates from before C++17. These days std::launder or equivalent should be added to the line noise. I don't have time to update the code to match right now.

我希望将接口与实现分开.这主要是为了保护使用库的代码免于该库的实现更改,尽管当然可以减少编译时间.

I am aspiring to separate interface from implementation. This is primarily to protect code using a library from changes in the implementation of said library, though reduced compilation times are certainly welcome.

对此的标准解决方案是实现习惯用法的指针,最有可能通过使用unique_ptr并仔细地与实现一起定义类析构函数来实现.

The standard solution to this is the pointer to implementation idiom, most likely to be implemented by using a unique_ptr and carefully defining the class destructor out of line, with the implementation.

不可避免地,这引起了对堆分配的担忧.我熟悉使它工作,然后使其快速",配置文件然后优化"和这样的智慧.网上也有文章,例如 gotw ,它声明明显的解决方法是脆弱且不可移植的.我有一个目前不包含任何堆分配的库-而且我想保持这种状态-所以还是让我们有一些代码.

Inevitably this raises concerns about heap allocation. I am familiar with "make it work, then make it fast", "profile then optimise" and such wisdom. There are also articles online, e.g. gotw, which declare the obvious workaround to be brittle and non-portable. I have a library which currently contains no heap allocations whatsoever - and I'd like to keep it that way - so let's have some code anyway.

#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's all in the implementation)
 private:
  // No private functions (they're also in the implementation)
  unsigned char state alignas(detail::alignment)[detail::capacity];
};

#endif

这对我来说似乎还不错.对齐方式和大小可以在实现中静态声明.我可以选择两者都高估(效率低下),或者如果它们发生变化(烦琐),则重新编译所有内容,但是这两种选择都不可怕.

This doesn't look too bad to me. Alignment and size can be statically asserted in the implementation. I can choose between overestimating both (inefficient) or recompiling everything if they change (tedious) - but neither option is terrible.

我不确定这种黑客是否会在存在继承的情况下工作,但是由于我不太喜欢接口中的继承,因此我不太介意.

I'm not certain this sort of hackery will work in the presence of inheritance, but as I don't much like inheritance in interfaces I don't mind too much.

如果我们大胆地假设我已经正确编写了实现(我将其添加到这篇文章中,但是这是未经测试的概念证明,所以还不是给定的),并且大小和对齐方式都大于或等于实现的实现,那么代码是否表现出实现已定义或未定义的行为?

If we boldly assume that I've written the implementation correctly (I'll append it to this post, but it's an untested proof of concept at this point so that's not a given), and both size and alignment are greater than or equal to that of the implementation, then does the code exhibit implementation defined, or undefined, behaviour?

#include "pimpl.hpp"
#include <cassert>
#include <vector>

// Usually a class that has behaviour we care about
// In this example, it's 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;
}

我知道那太可怕了.不过,我不介意使用代码生成器,因此不必重复输入.

I know that's pretty horrible. I don't mind using code generators though, so it's not something I'll have to type out repeatedly.

要明确说明这个冗长问题的症结,以下条件足以避免UB | IDB吗?

To state the crux of this over-long question explicitly, are the following conditions sufficient to avoid UB|IDB?

  • 状态大小与impl实例的大小匹配
  • 状态对齐与impl实例的对齐
  • 所有五项标准操作均根据impl的实施
  • 正确使用新的展示位置
  • 正确使用明确的析构函数调用

如果是的话,我将为Valgrind编写足够的测试,以清除演示中的几个错误.谢谢那些走这么远的人!

If they are, I'll write enough tests for Valgrind to flush out the several bugs in the demo. Thank you to any who get this far!

推荐答案

是的,这是完全安全且可移植的代码.

Yes, this is perfectly safe and portable code.

但是,没有必要在赋值运算符中使用新的布置和显式销毁.除了可以保证异常安全和高效之外,我认为仅使用example_impl的赋值运算符也更干净:

However, there is no need to use placement new and explicit destruction in your assignment operators. Aside from it being exception safe and more efficient, I'd argue it's also much cleaner to just use the assignment operator of 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数组,但我想那只是一个口味问题.

Personally, I also would use std::aligned_storage instead of an manually aligned char array, but I guess thats a matter of taste.

这篇关于无堆的皮普尔.不正确或迷信?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆