【问题标题】:rvalue reference with assignement operator带有赋值运算符的右值引用
【发布时间】:2012-01-13 09:29:23
【问题描述】:

本文http://cpp-next.com/archive/2009/08/want-speed-pass-by-value/comment-page-1/#comment-1877

T& T::operator=(T const& x) // x is a reference to the source
{ 
    T tmp(x);          // copy construction of tmp does the hard work
    swap(*this, tmp);  // trade our resources for tmp's
    return *this;      // our (old) resources get destroyed with tmp 
}

但鉴于复制省略,该公式显然效率低下!现在“很明显”编写复制和交换作业的正确方法是:

T& operator=(T x)    // x is a copy of the source; hard work already done
{
    swap(*this, x);  // trade our resources for x's
    return *this;    // our (old) resources get destroyed with x
}

据说为了避免复制省略,我们应该用值而不是const引用来编写赋值运算符。

使用右值引用功能,写赋值运算符是否更好? :

T& operator=(T&& x)  
{
    swap(*this, x);
    return *this;
}

终于没有区别了?

【问题讨论】:

    标签: c++ c++11 rvalue-reference


    【解决方案1】:

    有些类型在交换/分配习惯上的表现比其他类型更好。这是一个做得不好的:

    #include <cstddef>
    #include <new>
    #include <utility>
    
    template <class T>
    class MyVector
    {
        T* begin_;
        T* end_;
        T* capacity_;
    
    public:
        MyVector()
            : begin_(nullptr),
              end_(nullptr),
              capacity_(nullptr)
            {}
    
        ~MyVector()
        {
            clear();
            ::operator delete(begin_);
        }
    
        MyVector(std::size_t N, const T& t)
            : MyVector()
        {
            if (N > 0)
            {
                begin_ = end_ = static_cast<T*>(::operator new(N*sizeof(T)));
                capacity_ = begin_ + N;
                for (; N > 0; --N, ++end_)
                    ::new(end_) T(t);
            }
        }
    
        MyVector(const MyVector& v)
            : MyVector()
        {
            std::size_t N = v.size();
            if (N > 0)
            {
                begin_ = end_ = static_cast<T*>(::operator new(N*sizeof(T)));
                capacity_ = begin_ + N;
                for (std::size_t i = 0; i < N; ++i, ++end_)
                    ::new(end_) T(v[i]);
            }
        }
    
        MyVector(MyVector&& v)
            : begin_(v.begin_),
              end_(v.end_),
              capacity_(v.capacity_)
        {
            v.begin_ = nullptr;
            v.end_ = nullptr;
            v.capacity_ = nullptr;
        }
    
    #ifndef USE_SWAP_ASSIGNMENT
    
        MyVector& operator=(const MyVector& v)
        {
            if (this != &v)
            {
                std::size_t N = v.size();
                if (capacity() < N)
                {
                    clear();
                    ::operator delete(begin_);
                    begin_ = end_ = static_cast<T*>(::operator new(N*sizeof(T)));
                    capacity_ = begin_ + N;
                }
                std::size_t i = 0;
                T* p = begin_;
                for (; p < end_ && i < N; ++p, ++i)
                    (*this)[i] = v[i];
                if (i < N)
                {
                    for (; i < N; ++i, ++end_)
                        ::new(end_) T(v[i]);
                }
                else
                {
                    while (end_ > p)
                    {
                        --end_;
                        end_->~T();
                    }
                }
            }
            return *this;
        }
    
        MyVector& operator=(MyVector&& v)
        {
            clear();
            swap(v);
            return *this;
        }
    
    #else
    
        MyVector& operator=(MyVector v)
        {
            swap(v);
            return *this;
        }
    
    #endif
    
        void clear()
        {
            while (end_ > begin_)
            {
                --end_;
                end_->~T();
            }
        }
    
        std::size_t size() const
            {return static_cast<std::size_t>(end_ - begin_);}
        std::size_t capacity() const
            {return static_cast<std::size_t>(capacity_ - begin_);}
        const T& operator[](std::size_t i) const
            {return begin_[i];}
        T& operator[](std::size_t i)
            {return begin_[i];}
        void swap(MyVector& v)
        {
            std::swap(begin_, v.begin_);
            std::swap(end_, v.end_);
            std::swap(capacity_, v.capacity_);
        }
    };
    
    template <class T>
    inline
    void
    swap(MyVector<T>& x, MyVector<T>& y)
    {
        x.swap(y);
    }
    
    #include <iostream>
    #include <string>
    #include <chrono>
    
    int main()
    {
        MyVector<std::string> v1(1000, "1234567890123456789012345678901234567890");
        MyVector<std::string> v2(1000, "1234567890123456789012345678901234567890123456789");
        typedef std::chrono::high_resolution_clock Clock;
        typedef std::chrono::duration<double, std::micro> US;
        auto t0 = Clock::now();
        v2 = v1;
        auto t1 = Clock::now();
        std::cout << US(t1-t0).count() << " microseconds\n";
    
    }
    

    以下是我机器上的结果:

    $ clang++ -std=c++0x -stdlib=libc++ -O3  test.cpp
    $ a.out
    23.763 microseconds
    $ a.out
    23.322 microseconds
    $ a.out
    23.46 microseconds
    $ clang++ -std=c++0x -stdlib=libc++ -O3 -DUSE_SWAP_ASSIGNMENT test.cpp
    $ a.out
    176.452 microseconds
    $ a.out
    219.474 microseconds
    $ a.out
    178.15 microseconds
    

    我的观点:不要陷入相信银弹或“做任何事情的正确方法”的陷阱。复制/交换成语被超卖了。有时是合适的。但绝不是总是合适的。精心设计和仔细测试是无可替代的。

    【讨论】:

    • 如果每次测试时间为 1-5 秒,我会更满意您的结果。您的误差范围很高
    • high_resolution_clock 在这个平台上的精度是几纳秒。我正在显示数万纳秒,甚至高达数十万纳秒。我为每个案例展示了 3 次测试结果,让读者了解误差范围。一种情况比另一种情况快 7.7 倍。如果事实是一个案例的运行速度只比另一个快 6 倍,甚至只比另一个快两倍,甚至只比另一个快 20%,那么我的观点就是正确的。
    • 哦,是的。点绝对站得住脚。我只是在考虑统计...没关系,没关系。
    • 您能否总结一下性能问题的原因?今天我的脑子不太适应分析:)
    • 在这个测试用例中,复制赋值运算符(在#ifndef USE_SWAP_ASSIGNMENT 下)不会进入堆。使用复制/交换习语的那个。去堆是昂贵的。一个好的复制赋值运算符会尽可能重用堆内存等资源。
    【解决方案2】:

    想要对副本进行操作,否则您会从原始对象中删除信息。这个想法是从临时副本中删除信息。它不是很直观,但它允许您使用现有的复制构造函数和析构函数实现来完成op= 的繁重工作。

    复制省略不相关,因为在语义上需要复制时无法执行。

    在右值引用上操作可能没问题,因为如果您使用右值表达式作为 RHS 操作数调用 op=,那么它可能是一个临时对象,而调用范围可能不是不想/需要再使用它了。但是,假设这一点不是您的 op= 的工作。

    你的中间方法是规范的。

    T& operator=(T x)    // x is a copy of the source; hard work already done
    {
        swap(*this, x);  // trade our resources for x's
        return *this;    // our (old) resources get destroyed with x
    }
    

    【讨论】:

    • 那么我应该实施多少操作员分配才能高效并涵盖所有情况?
    • 就这一个。唯一更“有效”的方法是避免使用整个 swap 惯用语,并且一次只在两个对象之间分配一个成员。
    • 如果您没有性能问题,这是一个足够的答案。如果您的 std::lib 供应商使用此惯用语来分配诸如容器之类的东西,您将不得不解雇他。
    • @Lightness Races in Orbit:我已经添加了答案。
    【解决方案3】:

    有了右值引用特性,写赋值运算符是不是更好?

    这不是更好,因为这两个运算符不同(请参阅rule of five)。

    第一个 (T&amp; T::operator=(T const&amp; x)) 用于分配左值,而第二个 (T&amp; operator=(T&amp;&amp; x)) 用于分配右值。请注意,如果您只实现了第二个,这将无法编译:

    #include <iostream>
    
    struct T
    {
      T(int v):a(v){}
    
      T& operator=( const T& t)
      {
        std::cout<<"copy"<<std::endl;
        a=t.a;
        return *this;
      }
      T& operator=( T&& t)
      {
        std::cout<<"move"<<std::endl;
        a=std::move(t.a);
        return *this;
      }
    
      int a;
    };
    
    void foo( const T &t)
    {
      T tmp(2);
      std::cout<<tmp.a<<std::endl;
      tmp=t;
      std::cout<<tmp.a<<std::endl;
    }
    void bar(T &&t)
    {
      T tmp(2);
      std::cout<<tmp.a<<std::endl;
      tmp=std::move(t);
      std::cout<<tmp.a<<std::endl;
    }
    
    int main( void )
    {
      T t1(1);
      std::cout<<"foo"<<std::endl;
      foo(t1);
      std::cout<<"bar"<<std::endl;
      bar(T(5));
    }
    

    【讨论】:

    • 在 MSVC2010 下,复制 ctor 声明为私有且未实现,只有 T& operator=(T&& x) 声明上述代码编译
    • @Guillaume07 必须是扩展名
    • @Guillaume07:它可能实现了右值引用的早期草案。如果我没记错的话,最初它们被指定为绑定到右值和左值。
    猜你喜欢
    • 2011-12-12
    • 1970-01-01
    • 2016-12-03
    • 2011-12-15
    • 1970-01-01
    • 2010-12-18
    • 2011-08-02
    相关资源
    最近更新 更多