简体   繁体   English

使用C ++ 11复制和移动时避免代码重复

[英]Avoid code duplication when using C++11 copy & move

C++11 "move" is a nice feature, but I found it difficult to avoid code duplication (we all hate this) when used with "copy" at the same time. C ++ 11“移动”是一个很好的功能,但我发现当与“复制”同时使用时,很难避免代码重复(我们都讨厌这个)。 The following code is my implementation of a simple circular queue (incomplete), the two push() methods are almost the same except one line. 下面的代码是我实现的一个简单的循环队列(不完整),两个push()方法几乎相同,除了一行。

I have run into many similar situations like this. 我遇到过很多类似的情况。 Any ideas how to avoid this kind of code duplication without using macro ? 任何想法如何避免这种代码重复而不使用宏?

=== EDIT === ===编辑===

In this particular example, the duplicated code can be refactored out and put into a separate function, but sometimes this kind of refactoring is unavailable or cannot be easily implemented. 在这个特定的例子中,重复的代码可以被重构并放入一个单独的函数中,但有时这种重构是不可用的或者不能轻易实现。

#include <cstdlib>
#include <utility>

template<typename T>
class CircularQueue {
public:
    CircularQueue(long size = 32) : size{size} {
        buffer = std::malloc(sizeof(T) * size);
    }

    ~CircularQueue();

    bool full() const {
        return counter.in - counter.out >= size;
    }

    bool empty() const {
        return counter.in == counter.out;
    }

    void push(T&& data) {
        if (full()) {
            throw Invalid{};
        }
        long offset = counter.in % size;
        new (buffer + offset) T{std::forward<T>(data)};
        ++counter.in;
    }

    void push(const T& data) {
        if (full()) {
            throw Invalid{};
        }
        long offset = counter.in % size;
        new (buffer + offset) T{data};
        ++counter.in;
    }

private:
    T* buffer;
    long size;
    struct {
        long in, out;
    } counter;
};

The simplest solution here is to make the parameter a forwarding reference. 这里最简单的解决方案是使参数成为转发参考。 This way you can get away with only one function: 这样你就可以只使用一个函数:

template <class U>
void push(U&& data) {
    if (full()) {
        throw Invalid{};
    }
    long offset = counter.in % size;
    // please note here we construct a T object (the class template)
    // from an U object (the function template)
    new (buffer + offset) T{std::forward<U>(data)};
    ++counter.in;
}

There are disadvantages with method though: 但是方法有一些缺点:

  • it's not generic, that is it cannot always be done (in a trivial way). 它不是通用的,也就是说它不能总是这样做(以微不足道的方式)。 For instance when the parameter is not as simple as T (eg SomeType<T> ). 例如,当参数不像T那么简单时(例如SomeType<T> )。

  • You delay the type check of the parameter. 您延迟参数的类型检查。 Long and seemingly unrelated compiler error may follow when push is called with wrong parameter type. 当使用错误的参数类型调用push时,可能会出现长且看似无关的编译器错误。


By the way, in your example T&& is not a forwarding reference. 顺便说一句,在您的示例中, T&&不是转发引用。 It's an rvalue reference. 这是一个右值参考。 That's because T is not a template parameter of the function. 那是因为T不是函数的模板参数。 It's of the class so it's already deduced when the class is instantiated. 这是类,因此在实例化类时已经推断出它。 So the correct way to write your code would have been: 所以编写代码的正确方法是:

void push(T&& data) {
    ...
    ... T{std::move(data)};
    ...
}

void push(const T& data) {
   ... T{data};
   ...
}

The solution of using a forwarding reference is a good one. 使用转发参考的解决方案是一个很好的解决方案。 In some cases it gets difficult or annoying. 在某些情况下,它变得困难或烦人。 As a first step, wrap it with an interface that takes explicit types, then in the cpp file send them to a template implementation. 作为第一步,使用接受显式类型的接口包装它,然后在cpp文件中将它们发送到模板实现。

Now sometimes that first step fails as well: if there are N different arguments that all need to be forwarded into a container, this requires an interface of size 2^N, and possibly it has to cross multiple layers of interfaces to get to the implementation. 现在有时第一步也失败了:如果有N个不同的参数都需要转发到容器中,这需要一个大小为2 ^ N的接口,并且可能必须跨越多层接口才能实现。

To that end, instead of carrying or taking specific types, we can carry the end action(s) around. 为此,我们可以携带或采取特定类型,而不是携带或采取特定类型。 At the outermost interface, we convert arbitrary types into that/those action(s). 在最外层接口,我们将任意类型转换为那些/那些动作。

template<class T>
struct construct {
  T*(*action)(void* state,void* target)=nullptr;
  void* state=nullptr;
  construct()=default;
  construct(T&& t):
    action(
      [](void*src,void*targ)->T*{
        return new(targ) T( std::move(*static_cast<T*>(src)) );
      }
    ),
    state(std::addressof(t))
  {}
  construct(T const& t):
    action(
      [](void*src,void*targ)->T*{
        return new(targ) T( *static_cast<T const*>(src) );
      }
    ),
    state(const_cast<void*>(std::addressof(t)))
  {}
  T*operator()(void* target)&&{
    T* r = action(state,target);
    *this = {};
    return r;
  }
  explicit operator bool()const{return action;}
  construct(construct&&o):
    construct(o)
  {
    action=nullptr;
  }
  construct& operator=(construct&&o){
    *this = o;
    o.action = nullptr;
    return *this;
  }
private:
  construct(construct const&)=default;
  construct& operator=(construct const&)=default;
};

Once you have a construct<T> ctor object, you can construct an instance of T via std::move(ctor)(location) , where location is a pointer properly aligned to store a T with enough storage. 一旦你有了一个construct<T> ctor对象,你可以通过std::move(ctor)(location)构造一个T实例,其中location是一个正确对齐的指针,用于存储具有足够存储空间的T

A constructor<T> can be implicitly converted from a rvalue or lvalue T . 可以从rvalue或lvalue T隐式转换constructor<T> It can be enhanced with emplace support as well, but that requires a bunch more boilerplate to do correctly (or more overhead to do easily). 它也可以通过emplace支持来增强,但这需要更多的样板才能正确执行(或者更容易实现开销)。

Live example . 实例 The pattern is relatively simple type erasure. 该模式是相对简单的类型擦除。 We store the operation in a function pointer, and the data in a void pointer, and reconstruct the data from the void pointer in the stored action function pointer. 我们将操作存储在函数指针中,并将数据存储在void指针中,并从存储的动作函数指针中的void指针重构数据。

There is modest cost in the above type erasure/runtime concepts technique. 在上述类型的擦除/运行时概念技术中存在适度的成本。

We can also implement it like this: 我们也可以像这样实现它:

template<class T>
struct construct :
  private std::function< T*(void*) >
{
  using base = std::function< T*(void*) >;
  construct() = default;
  construct(T&& t):base(
    [&](void* target)mutable ->T* {
      return new(target) T(std::move(t));
    }
  ) {}
  construct(T const& t):base(
    [&](void* target)->T* {
      return new(target) T(t);
    }
  ) {}
  T* operator()(void* target)&&{
    T* r = base::operator()(target);
    (base&)(*this)={};
    return r;
  }
  explicit operator bool()const{
    return (bool)static_cast<base const&>(*this);
  }
};

which relies on std::function doing the type erasure for us. 它依赖于std::function为我们做类型擦除。

As this is designed to only work once (we move from the source), I force an rvalue context and eliminate my state. 由于这只是为了工作一次(我们从源头移动),我强制一个右值上下文并消除我的状态。 I also hide the fact I'm a std::function, because it doesn't follow those rules. 我还隐藏了我是std :: function的事实,因为它不符合这些规则。

Foreword 前言

Introducing code duplication when adding support of move semantics to your interface is very annoying. 在向界面添加移动语义支持时引入代码重复非常烦人。 For each function you have to make two almost identical implementations: the one which copies from the argument, and the one which moves from the argument. 对于每个函数,您必须创建两个几乎相同的实现:从参数复制的实现,以及从参数移动的实现。 If a function has two parameters, it's not even code duplication it's code quadruplication: 如果一个函数有两个参数,它甚至不是代码重复它的代码四倍:

void Func(const TArg1  &arg1, const TArg2  &arg2); // copies from both arguments
void Func(const TArg1  &arg1,       TArg2 &&arg2); // copies from the first, moves from the second
void Func(      TArg1 &&arg1, const TArg2  &arg2); // moves from the first, copies from the second
void Func(      TArg1 &&arg1,       TArg2 &&arg2); // moves from both

In general case you have to make up to 2^N overloads for a function where N is the number of parameters. 在一般情况下,您必须为函数补充2 ^ N次重载,其中N是参数的数量。 In my opinion this makes move semantics practically unusable. 在我看来,这使得移动语义几乎无法使用。 It is most disappointing feature of C++11. 这是C ++ 11最令人失望的功能。

The problem could have happened even earlier. 这个问题甚至可能更早发生。 Let's take a look at the following piece of code: 我们来看看下面这段代码:

void Func1(const T &arg);
T Func2();

int main()
{
    Func1(Func2());
    return 0;
}

It's quite strange that a temporary object is passed into the function that takes a reference. 将临时对象传递给接受引用的函数是很奇怪的。 The temporary object may even not have an address, it can be cached in a register for example. 临时对象甚至可能没有地址,例如它可以缓存在寄存器中。 But C++ allows to pass temporaries where a const (and only const) reference is accepted. 但是C ++允许传递临时值,其中接受const(并且只有const)引用。 In that case the lifetime of temporary is prolonged till the end of lifetime of the reference. 在这种情况下,临时的寿命会延长到参考寿命结束。 If there were no this rule, we would have to make two implementations even here: 如果没有这个规则,我们就必须在这里做两个实现:

void Func1(const T& arg);
void Func1(T arg);

I don't know why the rule that allows to pass temporaries where reference is accepted was created (well, if there were no this rule we would not be able to call copy constructor to make a copy of a temporary object, so Func1(Func2()) where Func1 is void Func1(T arg) would not work anyway :) ), but with this rule we don't have to make two overloads of the function. 我不知道为什么允许传递接受引用的临时值的规则被创建(好吧,如果没有这个规则我们将无法调用复制构造函数来制作临时对象的副本,所以Func1(Func2())其中Func1void Func1(T arg)无论如何都不会工作:)),但是使用这个规则我们不必对函数进行两次重载。

Solution #1: Perfect forwarding 解决方案#1:完美转发

Unfortunately there is no such simple rule which would make it unnecessary to implement two overloads of the same function: the one which takes a const lvalue reference and the one which takes a rvalue reference. 遗憾的是,没有这样的简单规则可以使得不必实现同一函数的两个重载:一个采用const左值引用,另一个采用rvalue引用。 Instead perfect forwarding was devised 相反,设计了完美的转发

template <typename U>
void Func(U &&param) // despite the fact the parameter has "U&&" type at declaration,
                     // it actually can be just "U&" or even "const U&", it’s due to
                     // the template type deducing rules
{
    value = std::forward<U>(param); // use move or copy semantic depending on the 
                                    // real type of param
}

It may look as that simple rule which allows to avoid duplication. 它可能看起来像允许避免重复的简单规则。 But it is not simple, it uses unobvious template "magic" to solve the problem, and also this solution has some disadvantages that follow from the fact that the function that uses perfect forwarding must be templated: 但它并不简单,它使用不明显的模板“魔法”来解决问题,而且这个解决方案也有一些缺点,这些缺点是必须模仿使用完美转发的功能:

  • The implementation of the function must be located in a header. 该函数的实现必须位于标题中。
  • It blows up the binary size because for each used combination of the parameters type (copy/move) it generates separate implementation (you have single implementation in the source code and at the same time you have up to 2^N implementations in the binary). 它会破坏二进制大小,因为对于每个使用的参数类型组合(复制/移动),它会生成单独的实现(您在源代码中有单个实现,同时在二进制文件中最多有2 ^ N个实现) 。
  • There is no type checking for the argument. 没有类型检查参数。 You can pass value of any type into the function (since the function accepts template type). 您可以将任何类型的值传递给函数(因为函数接受模板类型)。 The actual checking will be done at the points where parameter is actually used. 实际检查将在实际使用参数的点完成。 This may produce hard-to-understand error messages and lead to some unexpected consequences. 这可能会产生难以理解的错误消息,并导致一些意想不到的后果。

The last problem can be solved by creating non-template wrappers for perfect-forwarding functions: 最后一个问题可以通过为完美转发函数创建非模板包装器来解决:

public:
    void push(      T &&data) { push_fwd(data); }
    void push(const T  &data) { push_fwd(data); }

private:
    template <typename U>
    void push_fwd(U &&data)
    {
        // actual implementation
    }

Of course it can be used in practice only if the function has few parameters (one or two). 当然,只有当函数具有很少的参数(一个或两个)时,它才能在实践中使用。 Otherwise you have to make too many wrappers (up to 2^N, you know). 否则你必须制作太多的包装(最多2 ^ N,你知道)。

Solution #2: Runtime check for movability 解决方案#2:运行时检查可移动性

Eventually I got to the idea that checking arguments for movablity should be done not at compile-time but at runtime. 最后我认为检查movablity的参数不应该在编译时进行,而是在运行时进行。 I created some reference-wrapper class with constructors that took both types of references (rvalue and const lvalue). 我创建了一些带有构造函数的reference-wrapper类,它们采用了两种类型的引用(rvalue和const lvalue)。 The class stored the passed to constructor reference as const lvalue reference and additionally it stored the flag whether the passed reference was rvalue. 该类将传递给构造函数的引用存储为const左值引用,另外它存储了标志,传递的引用是否为rvalue。 Then you could check at runtime whether the original reference was rvalue and if so you just casted the stored reference to rvalue-reference. 然后你可以在运行时检查原始引用是否是rvalue,如果是,你只需将存储的引用转换为rvalue-reference。

Unsurprisingly someone else had got to this idea before me. 不出所料,别人在我面前得到了这个想法。 He named this as "in idiom" (I called this "pmp" - possibly movable param). 他将此命名为“成语”(我称之为“pmp” - 可能是可移动的参数)。 You can read about this idiom in details here and here (original page about "in" idiom, I recommend to read all 3 parts of article if you are really interested in problem, the article reviews the problem in depth). 你可以在这里这里详细阅读这个成语(关于“in”成语的原始页面,如果你真的对问题感兴趣,我建议阅读文章的所有3部分,文章深入探讨了这个问题)。

In short the implementation of the idiom looks like this: 简而言之,这个成语的实现看起来像这样:

template <typename T> 
class in
{
public:
  in (const T& l): v_ (l), rv_ (false) {}
  in (T&& r): v_ (r), rv_ (true) {}

  bool rvalue () const {return rv_;}

  const T& get () const {return v_;}
  T&& rget () const {return std::move (const_cast<T&> (v_));}

private:
  const T& v_; // original reference
  bool rv_;    // whether it is rvalue-reference
};

(Full implementation also contains special case when some types can be implicitly converted into T) (完全实现还包含特殊情况,当某些类型可以隐式转换为T)

Example of usage: 用法示例:

class A
{
public:
  void set_vec(in<std::vector<int>> param1, in<std::vector<int>> param2)
  {
      if (param1.rvalue()) vec1 = param1.rget(); // move if param1 is rvalue
      else                 vec1 = param1.get();  // just copy otherwise
      if (param2.rvalue()) vec2 = param2.rget(); // move if param2 is rvalue
      else                 vec2 = param2.get();  // just copy otherwise
  }
private:
  std::vector<int> vec1, vec2;
};

The implementation of "in" lacks copy and move constructors. “in”的实现缺少复制和移动构造函数。

class in
{
  ...
  in(const in  &other): v_(other.v_), rv_(false)     {} // always makes parameter not movable
                                                        // even if the original reference
                                                        // is movable
  in(      in &&other): v_(other.v_), rv_(other.rv_) {} // makes parameter movable if the
                                                        // original reference was is movable
  ...
};

Now we can use it in this way: 现在我们可以这样使用它:

void func1(in<std::vector<int>> param);
void func2(in<std::vector<int>> param);

void func3(in<std::vector<int>> param)
{
    func1(param); // don't move param into func1 even if original reference
                  // is rvalue. func1 will always use copy of param, since we
                  // still need param in this function

    // some usage of param

    // now we don’t need param
    func2(std::move(param)); // move param into func2 if original reference
                             // is rvalue, or copy param into func2 if original
                             // reference is const lvalue
}

We could also overload an assignment operator: 我们还可以重载赋值运算符:

template<typename T>
T& operator=(T &lhs, in<T> rhs)
{
    if (rhs.rvalue()) lhs = rhs.rget();
    else              lhs = rhs.get();
    return lhs;
}

After that we would not need to check for ravlue each time, we could just use it in this way: 之后我们不需要每次检查ravlue,我们可以这样使用它:

   vec1 = std::move(param1); // moves or copies depending on whether param1 is movable
   vec2 = std::move(param2); // moves or copies depending on whether param2 is movable

But unfortunately C++ doesn't allow overload of operator= as global function ( https://stackoverflow.com/a/871290/5447906 ). 但不幸的是,C ++不允许operator=作为全局函数重载( https://stackoverflow.com/a/871290/5447906 )。 But we can rename this function into assign : 但我们可以将此函数重命名为assign

template<typename T>
void assign(T &lhs, in<T> rhs)
{
    if (rhs.rvalue()) lhs = rhs.rget();
    else              lhs = rhs.get();
}

and use it like this: 并像这样使用它:

    assign(vec1, std::move(param1)); // moves or copies depending on whether param1 is movable
    assign(vec2, std::move(param2)); // moves or copies depending on whether param2 is movable

Also this won't work with constructors. 这也不适用于构造函数。 We can't just write: 我们不能只写:

std::vector<int> vec(std::move(param));

This requires the standard library to support this feature: 这需要标准库来支持此功能:

class vector
{
    ...
public:
    vector(std::in<vector> other); // copy and move constructor
    ...
}

But standards doesn't know anything about our "in" class. 但是标准对我们的“in”课程一无所知。 And here we can't make workaround similar to assign , so the usage of the "in" class is limited. 在这里,我们不能使解决方法类似于assign ,因此“in”类的使用是有限的。

Afterword 后记

T , const T& , T&& for parameters it's too many for me. Tconst T&T&&参数对我来说太多了。 Stop introducing things that do the same (well, almost the same). 停止介绍做同样的事情(好吧,几乎一样)。 T is just enough! T就够了!

I would prefer to write just like this: 我更喜欢这样写:

// The function in ++++C language:
func(std::vector<int> param) // no need to specify const & or &&, param is just parameter.
                             // it is always reference for complex types (or for types with
                             // special qualifier that says that arguments of this type
                             // must be always passed by reference).
{
    another_vec = std::move(param); // move parameter if it's movable.
                                    // compiler hides actual rvalue-ness
                                    // of the arguments in its ABI
}

I don't know if standard committee considered this sort of move semantics implementation but it's probably too late to make such changes in C++ because they would make ABI of compilers incompatible with previous versions. 我不知道标准委员会是否考虑过这种移动语义实现,但是在C ++中进行这样的更改可能为时已晚,因为它们会使编译器的ABI与以前的版本不兼容。 Also it adds some runtime overhead, and there may be other problems that we don't know. 它还增加了一些运行时开销,可能还有其他一些我们不知道的问题。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM