首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >使用C++11复制和移动时避免代码重复

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

Stack Overflow用户
提问于 2015-12-25 08:44:57
回答 3查看 2.9K关注 0票数 24

C++11 "move“是一个很好的特性,但我发现当与”复制“同时使用时,很难避免代码重复(我们都讨厌这样做)。下面的代码是我实现的一个简单的循环队列(不完全),除了一行之外,两个push()方法几乎是相同的。

我遇到过很多类似的情况。如何避免这种不使用宏的代码重复呢?

===编辑===

在这个特殊的例子中,重复的代码可以重构并放入一个单独的函数中,但是有时这种重构是不可用的,或者很难实现。

代码语言:javascript
复制
#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;
};
EN

回答 3

Stack Overflow用户

回答已采纳

发布于 2015-12-25 10:25:20

这里最简单的解决方案是使参数成为转发引用。这样您就可以只使用一个函数就可以了:

代码语言:javascript
复制
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;
}

然而,方法也有缺点:

  • 它不是通用的,也就是说它不能总是被完成(以一种琐碎的方式)。例如,当参数不像T那么简单时(例如,SomeType<T>)。
  • 延迟参数的类型检查。当使用错误的参数类型调用push时,可能会出现长且看似无关的编译器错误。

顺便说一下,在您的示例中,T&&不是转发引用。这是一个参考价值。这是因为T不是函数的模板参数。它是类的,所以它已经在类实例化时推导出来了。因此,编写代码的正确方法应该是:

代码语言:javascript
复制
void push(T&& data) {
    ...
    ... T{std::move(data)};
    ...
}

void push(const T& data) {
   ... T{data};
   ...
}
票数 16
EN

Stack Overflow用户

发布于 2015-12-25 13:03:33

使用转发引用的解决方案是一个很好的解决方案。在某些情况下,它变得困难或烦人。作为第一步,用一个采用显式类型的接口包装它,然后在cpp文件中将它们发送到一个模板实现。

现在,有时第一步也会失败:如果有N个不同的参数都需要转发到容器中,这就需要一个大小为2^N的接口,并且可能需要跨多个接口层才能到达实现。

为了达到这个目的,我们不需要携带或采取特定的类型,我们可以随身携带结束动作。在最外层的接口上,我们将任意类型转换为该/那些操作。

代码语言:javascript
复制
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;
};

一旦有了construct<T> ctor对象,就可以通过std::move(ctor)(location)构造一个T实例,其中位置是一个指针,正确地对齐以存储具有足够存储空间的T

constructor<T>可以从rvalue或lvalue T隐式转换。它也可以通过emplace支持来增强,但这需要更多的样板才能正确地完成(或者需要更多的开销来轻松完成)。

实例化。该模式是相对简单的类型擦除。我们将操作存储在函数指针中,将数据存储在空指针中,并从存储的操作函数指针中的空指针中重构数据。

上述类型的擦除/运行时概念技术的成本并不高。

我们也可以这样实施:

代码语言:javascript
复制
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);
  }
};

它依赖于std::function为我们做类型擦除。

因为它设计为只工作一次(我们从源),我强制一个rvalue上下文并消除我的状态。我还隐藏了我是一个std::function的事实,因为它不遵循这些规则。

票数 4
EN

Stack Overflow用户

发布于 2016-04-26 03:37:34

前言

在向界面添加移动语义支持时引入代码复制是非常烦人的。对于每个函数,您必须做两个几乎相同的实现:一个从参数复制,另一个从参数移动。如果一个函数有两个参数,那么它甚至不是代码复制--它是代码四倍乘法:

代码语言:javascript
复制
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

通常情况下,对于一个函数,如果N是参数的数目,则必须对其进行最多2^N的重载。在我看来,这使得移动语义几乎不可用。这是C++11最令人失望的特性。

这个问题可能会发生得更早。让我们看一看下面的代码:

代码语言:javascript
复制
void Func1(const T &arg);
T Func2();

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

非常奇怪的是,临时对象被传递到接受引用的函数中。临时对象甚至可能没有地址,例如可以缓存在寄存器中。但是C++允许在接受const (而且只有const)引用的情况下传递临时引用。在这种情况下,临时的生存期被延长到引用的生存期结束。如果没有这条规则,即使在这里,我们也必须进行两种实现:

代码语言:javascript
复制
void Func1(const T& arg);
void Func1(T arg);

我不知道为什么创建了允许传递引用的临时规则(如果没有此规则,我们就无法调用复制构造函数来创建临时对象的副本,因此Func1(Func2()) ( Func1 is void Func1(T arg) )无论如何都不能工作: ),但是使用这个规则,我们不必对函数进行两个重载。

解决方案#1:完美转发

不幸的是,没有这样简单的规则,因此没有必要实现相同函数的两个重载:一个接受const值引用,另一个接受rvalue引用。相反,完美的转发设计出来了。

代码语言:javascript
复制
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
}

这似乎是一条简单的规则,可以避免重复。但是它并不简单,它使用了不明显的模板“魔术”来解决这个问题,而且该解决方案也有一些缺点,因为使用完美转发的功能必须模板化:

  • 函数的实现必须位于标头中。
  • 它破坏了二进制大小,因为对于参数类型(复制/移动)的每一次使用的组合,它都会生成单独的实现(源代码中有单个实现,而二进制文件中则有多达2^N的实现)。
  • 没有对参数进行类型检查。您可以将任何类型的值传递给函数(因为该函数接受模板类型)。实际检查将在实际使用参数的点进行。这可能会产生难以理解的错误消息,并导致一些意想不到的后果。

最后一个问题可以通过为完善的转发功能创建非模板包装器来解决:

代码语言:javascript
复制
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
    }

当然,只有当函数的参数很少(一个或两个)时,它才能在实践中使用。否则,你必须制造太多的包装(最多2^N,你知道)。

解决方案2:运行时检查可移动性

最后,我有了这样的想法:检查参数的动态性不应该在编译时,而是在运行时。我使用两个类型的引用(rvalue和const )构造函数创建了一些引用包装类。类将传递到构造函数的引用存储为constructor引用,此外,它还存储传递的引用是否为rvalue的标志。然后,您可以在运行时检查原始引用是否为rvalue,如果是,您只需将存储的引用转储到rvalue-引用。

不出所料,在我之前就有人想到了这个主意。他把它命名为“成语”(我称之为"pmp“--可能是可移动的param)。你可以详细阅读这个成语这里这里 (关于" in“成语的原始页面,我建议您阅读文章的所有三个部分,如果您真的对问题感兴趣,文章将深入地回顾这个问题)。

简而言之,这个成语的实现方式如下:

代码语言:javascript
复制
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
};

(完全实现还包含一些类型可以隐式转换为T的特殊情况)

使用示例:

代码语言:javascript
复制
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;
};

"in“的实现缺少复制和移动构造函数。

代码语言:javascript
复制
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
  ...
};

现在我们可以这样使用它了:

代码语言:javascript
复制
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
}

我们还可以重载赋值操作符:

代码语言:javascript
复制
template<typename T>
T& operator=(T &lhs, in<T> rhs)
{
    if (rhs.rvalue()) lhs = rhs.rget();
    else              lhs = rhs.get();
    return lhs;
}

在此之后,我们不需要每次检查是否有蹂躏,我们可以这样使用它:

代码语言:javascript
复制
   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

但是不幸的是,C++不允许operator=过载作为全局函数(https://stackoverflow.com/a/871290/5447906)。但是我们可以将这个函数重命名为assign

代码语言:javascript
复制
template<typename T>
void assign(T &lhs, in<T> rhs)
{
    if (rhs.rvalue()) lhs = rhs.rget();
    else              lhs = rhs.get();
}

像这样使用它:

代码语言:javascript
复制
    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

而且,这将不适用于构造函数。我们不能只写:

代码语言:javascript
复制
std::vector<int> vec(std::move(param));

这需要标准库来支持这个特性:

代码语言:javascript
复制
class vector
{
    ...
public:
    vector(std::in<vector> other); // copy and move constructor
    ...
}

但是标准对我们的“在”类一无所知。在这里,我们无法做出类似于assign的解决方案,因此"in“类的使用是有限的。

后缀词

Tconst T&T&&参数--对我来说太多了。停止介绍同样的事情(嗯,几乎一样)。T就够了!

我宁愿这样写:

代码语言:javascript
复制
// 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
}

我不知道标准委员会是否考虑过这种移动语义实现,但是在C++中进行这样的更改可能为时已晚,因为它们会使编译器的ABI与以前的版本不兼容。此外,它还增加了一些运行时开销,可能还有其他我们不知道的问题。

票数 0
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/34461376

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档