Implicit Move Must Go

Implicit Move Must Go
– 转载出处
Document Number: N3153=10-0143

Document Date: 2010-10-17

Author: Dave Abrahams dave@boostpro.com

What is Implicit Move?

In N2855, the authors described a surprising effect that occurs when “legacy” C++03 types are combined in the same object with move-enabled C++0x types: the combined type could acquire a throwing move constructor. At that time, we didn’t have a way to implement vector::push_back and a few other important strong-guarantee operations in the presence of a throwing move.

Several independent efforts were made to deal with this problem. One approach, while not a complete solution, shrank the problem space considerably and had many other benefits: implicitly generate move constructors and move assignment operators when not supplied by the user, in much the same way that we do for copy constructors and copy assignment operators. That is the implicit move feature, and it was voted into the working paper this past spring.

Further Background

Another effort, spurred on by Rani Sharoni’s comments at C++Next finally yielded a complete, standalone solution to the problem, a form of which was eventually also adopted by the committee. Nobody attempted to “repeal” the implicit generation of move operations from the standard document, not least because of those “many other benefits” I alluded to earlier. But now we have a problem that we hadn’t anticipated.

The Problem

Back in August, Scott Meyers posted to comp.lang.c++ about a problem where implicit generation of move constructors could break C++03 class invariants. For example, the following valid C++03 program would be broken under the current C++0x rules (and indeed, is broken in g++4.6 with --std=c++0x) by the implicit generation of a move constructor:

#define _GLIBCXX_DEBUG
#include <iostream>
#include <vector>
struct X{
    // invariant: v.size() == 5
    X() : v(5) {}
 
    ~X()
    {
        std::cout << v[0] << std::endl;
    }
 
 private:    
    std::vector<int> v;
};
 int main()
{
    std::vector<X> y;
    y.push_back(X()); // X() rvalue: copied in C++03, moved in C++0x
}

The key problem here is that in C++03, X had an invariant that its v member always had 5 elements. X::~X() counted on that invariant, but the newly-introduced move constructor moved from v, thereby setting its length to zero.

Tweak #1: Destructors Suppress Implicit Move

Because rvalues are generally “about to be destroyed,” and the broken invariant was only detected in X‘s destructor, it’s tempting to think that we can “tweak” the current rules by preventing the generation of implicit move constructors when a user-defined destructor is present. However, the following example would still break (and breaks under g++4.6 with --std=c++0x):

#define _GLIBCXX_DEBUG
#include <algorithm>
#include <iostream>
#include <vector>
 struct Y{
    // invariant: values.size() > 0
    Y() : values(1, i++) {}
    Y(int n) : values(1,n) {}
 
    bool operator==(Y const& rhs) const
    { 
        return this->values == rhs.values;
    }
 
    int operator[](unsigned i) const
    { return values[i]; }
 
 private:
    static int i;
    std::vector<int> values;
};
 int Y::i = 0;
 int main()
{   
    Y ys[10];
    std::remove(&ys[0], &ys[0]+10, Y(5));
    std::cout << ys[9][0];
};

In C++03, there’s no way to create a Y with a zero-length values member, but because std::remove is allowed to use move operations in C++0x, it can leave a moved-from Y at the end of the array, and that could be empty, causing undefined behavior in the last line.

About the use of **std::remove** in these examples

In C++03, std::remove eliminates values from a range by assigning over them (technically it can also use swap, but let’s assume assignment for now). Since it can’t actually change sequence structure, it assigns over the unwanted elements with values from later in the sequence, pushing everything toward the front until there’s a subrange containing only what’s desired, and returns the new end iterator of that subrange . For example, after removeing 0 from the sequence 0 1 2 0 5, we’d end up with 1 2 5, and then 0 5—the last two elements of the sequence would be unchanged.

In C++0x, we have move semantics, and std::remove has permission to use move assignment. So in C++0x, we’d end up with 1 2 5 0 x at the end of the sequence, where x is the value left over after moving from the last element—if the elements are ints, that would be 5, but if they are BigNums, it could be anything. Similar properties are shared by std::remove_if and std::unique.

There’s another way moved-from values can be exposed to C++03 code running under C++0x: an algorithm such as sort can throw an exception while shuffling elements, and you can then observe a state where not everything has been moved back into place. Showing that just makes for more complicated examples, however.

Tweak #2: Constructors Suppress Implicit Move

It’s also tempting to think that at least in classes without a user-defined constructor, we could safely conclude that there’s no intention to maintain an invariant, but that reasoning, too, is flawed:

#define _GLIBCXX_DEBUG
#include <iostream>
#include <vector>
 // An always-initialized wrapper for unsigned int
struct Number{
    Number(unsigned x = 0) : value(x) {}
    operator unsigned() const { return value; }
 private:
    unsigned value;
};
 struct Y{
    // Invariant: length == values.size().  Default ctor is fine.
 
    // Maintains the invariant
    void resize(unsigned n)
    {
        std::vector<int> s(n);
        swap(s,values);
        length = Number(n);
    }
 
    bool operator==(Y const& rhs) const
    { 
        return this->values == rhs.values;
    }
 
    friend std::ostream& operator<<(std::ostream& s, Y const& a)
    {
        for (unsigned i = 0; i < a.length; ++i)
            std::cout << a.values[i] << " ";
        return s;
    };
 
 private:
    std::vector<int> values;
    Number length;
};
 int main()
{   
    std::vector<Y> z(1, Y());
 
    Y a;
    a.resize(2);
    z.push_back(a);
 
    std::remove(z.begin(), z.end(), Y());
    std::cout << z[1] << std::endl;
};

In this case, the invariant that length == values.size() was established by the well-understood default-construction behavior of subobjects, but implicit move generation has violated it.

Tweak #3: Private Members Suppress Implicit Move

It’s also tempting to think that we can use private members to indicate that an invariant needs to be preserved; that a “C-style struct” is not encapsulated and has no need of protection. But the members of a privately-inherited struct are effectively encapsulated and private with respect to the derived class. It is not uncommon to see members moved into an implementation detail struct, which is then used as a base class:

// Modified fragment of previous example.  Replace the definition
// of Y with this code.
 namespace detail{
  struct Y_impl
  {
      std::vector<int> values;
      Number length;
  }
}
 struct Y : private Y_impl{
    void resize(unsigned n)
    {
        std::vector<int> s(n);
        swap(s,values);
        length = Number(n);
    }
 
    bool operator==(Y const& rhs) const
    { 
        return this->values == rhs.values;
    }
 
    friend std::ostream& operator<<(std::ostream& s, Y const& a)
    {
        for (unsigned i = 0; i < a.length; ++i)
            std::cout << a.values[i] << " ";
        return s;
    };
};

Real-World Examples

Of course these examples are all somewhat contrived, but they are not unrealistic. We’ve already found classes in the (new) standard library—std::piecewise_linear_distribution::param_type and std::piecewise_linear_distribution—that have been implemented in exactly such a way as to expose the same problems. In particular, they were shipped with g++4.5, which had no explicit move, and not updated for g++4.6, which did. Thus they were broken by the introduction of implicitly-generated move constructors.

Summary

I actually like implicit move. It would be a very good idea in a new language, where we didn’t have legacy code to consider. Unfortunately, it breaks fairly pedestrian-looking C++03 examples. We could continue to explore tweaks to the rules for implicit move generation, but each tweak we need to make eliminates implicit move for another category of types where it could have been useful, and weakens confidence that we have analyzed the situation correctly. And it’s very late in the standardization process to tolerate such uncertainty.

Conclusions

It is time to remove implicitly-generated move operations from the draft. That suggestion may seem radical, but implicit move was proposed very late in the process on the premise that it “treated…the root cause” of the exception-safety issues revealed in N2855. However, it did not treat those causes: we still needed noexcept. Therefore, implicitly-generated move operations can be removed without fundamentally undermining the usefulness or safety of rvalue references.

The default semantics of the proposed implicit move operations are still quite useful and commonly-needed. Therefore, while removing implicit generation, we should retain the ability to produce those semantics with “= default.” It would also be nice if the rules allowed a more concise way to say “give me all the defaults for move and copy assignment,” but this paper offers no such proposal.Implicit Move Must Go

Document Number: N3153=10-0143

Document Date: 2010-10-17

Author: Dave Abrahams dave@boostpro.com

What is Implicit Move?

In N2855, the authors described a surprising effect that occurs when “legacy” C++03 types are combined in the same object with move-enabled C++0x types: the combined type could acquire a throwing move constructor. At that time, we didn’t have a way to implement vector::push_back and a few other important strong-guarantee operations in the presence of a throwing move.

Several independent efforts were made to deal with this problem. One approach, while not a complete solution, shrank the problem space considerably and had many other benefits: implicitly generate move constructors and move assignment operators when not supplied by the user, in much the same way that we do for copy constructors and copy assignment operators. That is the implicit move feature, and it was voted into the working paper this past spring.

Further Background

Another effort, spurred on by Rani Sharoni’s comments at C++Next finally yielded a complete, standalone solution to the problem, a form of which was eventually also adopted by the committee. Nobody attempted to “repeal” the implicit generation of move operations from the standard document, not least because of those “many other benefits” I alluded to earlier. But now we have a problem that we hadn’t anticipated.

The Problem

Back in August, Scott Meyers posted to comp.lang.c++ about a problem where implicit generation of move constructors could break C++03 class invariants. For example, the following valid C++03 program would be broken under the current C++0x rules (and indeed, is broken in g++4.6 with --std=c++0x) by the implicit generation of a move constructor:

#define _GLIBCXX_DEBUG
#include <iostream>
#include <vector>
struct X{
    // invariant: v.size() == 5
    X() : v(5) {}
 
    ~X()
    {
        std::cout << v[0] << std::endl;
    }
 
 private:    
    std::vector<int> v;
};
 int main()
{
    std::vector<X> y;
    y.push_back(X()); // X() rvalue: copied in C++03, moved in C++0x
}

The key problem here is that in C++03, X had an invariant that its v member always had 5 elements. X::~X() counted on that invariant, but the newly-introduced move constructor moved from v, thereby setting its length to zero.

Tweak #1: Destructors Suppress Implicit Move

Because rvalues are generally “about to be destroyed,” and the broken invariant was only detected in X‘s destructor, it’s tempting to think that we can “tweak” the current rules by preventing the generation of implicit move constructors when a user-defined destructor is present. However, the following example would still break (and breaks under g++4.6 with --std=c++0x):

#define _GLIBCXX_DEBUG
#include <algorithm>
#include <iostream>
#include <vector>
 struct Y{
    // invariant: values.size() > 0
    Y() : values(1, i++) {}
    Y(int n) : values(1,n) {}
 
    bool operator==(Y const& rhs) const
    { 
        return this->values == rhs.values;
    }
 
    int operator[](unsigned i) const
    { return values[i]; }
 
 private:
    static int i;
    std::vector<int> values;
};
 int Y::i = 0;
 int main()
{   
    Y ys[10];
    std::remove(&ys[0], &ys[0]+10, Y(5));
    std::cout << ys[9][0];
};

In C++03, there’s no way to create a Y with a zero-length values member, but because std::remove is allowed to use move operations in C++0x, it can leave a moved-from Y at the end of the array, and that could be empty, causing undefined behavior in the last line.

About the use of **std::remove** in these examples

In C++03, std::remove eliminates values from a range by assigning over them (technically it can also use swap, but let’s assume assignment for now). Since it can’t actually change sequence structure, it assigns over the unwanted elements with values from later in the sequence, pushing everything toward the front until there’s a subrange containing only what’s desired, and returns the new end iterator of that subrange . For example, after removeing 0 from the sequence 0 1 2 0 5, we’d end up with 1 2 5, and then 0 5—the last two elements of the sequence would be unchanged.

In C++0x, we have move semantics, and std::remove has permission to use move assignment. So in C++0x, we’d end up with 1 2 5 0 x at the end of the sequence, where x is the value left over after moving from the last element—if the elements are ints, that would be 5, but if they are BigNums, it could be anything. Similar properties are shared by std::remove_if and std::unique.

There’s another way moved-from values can be exposed to C++03 code running under C++0x: an algorithm such as sort can throw an exception while shuffling elements, and you can then observe a state where not everything has been moved back into place. Showing that just makes for more complicated examples, however.

Tweak #2: Constructors Suppress Implicit Move

It’s also tempting to think that at least in classes without a user-defined constructor, we could safely conclude that there’s no intention to maintain an invariant, but that reasoning, too, is flawed:

#define _GLIBCXX_DEBUG
#include <iostream>
#include <vector>
 // An always-initialized wrapper for unsigned int
struct Number{
    Number(unsigned x = 0) : value(x) {}
    operator unsigned() const { return value; }
 private:
    unsigned value;
};
 struct Y{
    // Invariant: length == values.size().  Default ctor is fine.
 
    // Maintains the invariant
    void resize(unsigned n)
    {
        std::vector<int> s(n);
        swap(s,values);
        length = Number(n);
    }
 
    bool operator==(Y const& rhs) const
    { 
        return this->values == rhs.values;
    }
 
    friend std::ostream& operator<<(std::ostream& s, Y const& a)
    {
        for (unsigned i = 0; i < a.length; ++i)
            std::cout << a.values[i] << " ";
        return s;
    };
 
 private:
    std::vector<int> values;
    Number length;
};
 int main()
{   
    std::vector<Y> z(1, Y());
 
    Y a;
    a.resize(2);
    z.push_back(a);
 
    std::remove(z.begin(), z.end(), Y());
    std::cout << z[1] << std::endl;
};

In this case, the invariant that length == values.size() was established by the well-understood default-construction behavior of subobjects, but implicit move generation has violated it.

Tweak #3: Private Members Suppress Implicit Move

It’s also tempting to think that we can use private members to indicate that an invariant needs to be preserved; that a “C-style struct” is not encapsulated and has no need of protection. But the members of a privately-inherited struct are effectively encapsulated and private with respect to the derived class. It is not uncommon to see members moved into an implementation detail struct, which is then used as a base class:

// Modified fragment of previous example.  Replace the definition
// of Y with this code.
 namespace detail{
  struct Y_impl
  {
      std::vector<int> values;
      Number length;
  }
}
 struct Y : private Y_impl{
    void resize(unsigned n)
    {
        std::vector<int> s(n);
        swap(s,values);
        length = Number(n);
    }
 
    bool operator==(Y const& rhs) const
    { 
        return this->values == rhs.values;
    }
 
    friend std::ostream& operator<<(std::ostream& s, Y const& a)
    {
        for (unsigned i = 0; i < a.length; ++i)
            std::cout << a.values[i] << " ";
        return s;
    };
};

Real-World Examples

Of course these examples are all somewhat contrived, but they are not unrealistic. We’ve already found classes in the (new) standard library—std::piecewise_linear_distribution::param_type and std::piecewise_linear_distribution—that have been implemented in exactly such a way as to expose the same problems. In particular, they were shipped with g++4.5, which had no explicit move, and not updated for g++4.6, which did. Thus they were broken by the introduction of implicitly-generated move constructors.

Summary

I actually like implicit move. It would be a very good idea in a new language, where we didn’t have legacy code to consider. Unfortunately, it breaks fairly pedestrian-looking C++03 examples. We could continue to explore tweaks to the rules for implicit move generation, but each tweak we need to make eliminates implicit move for another category of types where it could have been useful, and weakens confidence that we have analyzed the situation correctly. And it’s very late in the standardization process to tolerate such uncertainty.

Conclusions

It is time to remove implicitly-generated move operations from the draft. That suggestion may seem radical, but implicit move was proposed very late in the process on the premise that it “treated…the root cause” of the exception-safety issues revealed in N2855. However, it did not treat those causes: we still needed noexcept. Therefore, implicitly-generated move operations can be removed without fundamentally undermining the usefulness or safety of rvalue references.

The default semantics of the proposed implicit move operations are still quite useful and commonly-needed. Therefore, while removing implicit generation, we should retain the ability to produce those semantics with “= default.” It would also be nice if the rules allowed a more concise way to say “give me all the defaults for move and copy assignment,” but this paper offers no such proposal.

内容概要:该论文深入研究了液压挖掘机动臂下降势能回收技术,旨在解决传统液压挖掘机能耗高的问题。提出了一种新型闭式回路势能回收系统,利用模糊PI自整定控制算法控制永磁无刷直流电动机,实现了变转速容积调速控制,消除了节流和溢流损失。通过建立数学模型和仿真模型,分析了不同负载下的系统性能,并开发了试验平台验证系统的高效性和节能效果。研究还涵盖了执行机构能量分布分析、系统元件参数匹配及电机控制性能优化,为液压挖掘机节能技术提供了理论和实践依据。此外,通过实验验证,该系统相比传统方案可降低28%的能耗,控制系统响应时间缩短40%,为工程机械的绿色化、智能化发展提供了关键技术支撑。 适合人群:从事工程机械设计、制造及维护的工程师和技术人员,以及对液压系统节能技术感兴趣的科研人员。 使用场景及目标:①理解液压挖掘机闭式回路动臂势能回收系统的原理和优势;②掌握模糊PI自整定控制算法的具体实现;③学习如何通过理论建模、仿真和实验验证来评估和优化液压系统的性能。 其他说明:此研究不仅提供了详细的理论分析和数学建模,还给出了具体的仿真代码和实验数据,便于读者在实际工作中进行参考和应用。研究结果表明,该系统不仅能显著提高能源利用效率,还能延长设备使用寿命,降低维护成本,具有重要的工程应用价值。
### C# 关键字分类 对于给定的关键字列表,在C#中这些关键字可以根据其性质分为两类:保留字和上下文关键字。 #### 保留字(Reserved Keywords) 保留字是在任何情况下都具有特殊意义的单词,不能作为标识符使用。以下是属于保留字的部分关键字: - `int`[^1] - `class` - `float` - `double` - `char` - `string` - `bool` - `void` #### 上下文关键字(Contextual Keywords) 上下文关键字只有在特定语境中有特殊含义,而在其他地方可以用作标识符。这里列举了一些可能被认为是上下文关键字的例子: - `public` - `private` - `protected` - `static` - `new` - `if` - `else` - `switch` 请注意上述分类基于提供的关键字列表,并且部分关键字如`if`, `else`, 和 `switch`通常也被视为保留字而非仅限于上下文关键字。 ### IMPLICIT 关键字解析 关于`implicit`关键字的应用主要体现在几种不同的编程语言环境中,下面分别介绍不同语言里该关键字的作用。 #### C++ 在C++中,`explicit`用于防止编译器执行某些隐式的类型转换操作,而`implicit`并不是一个合法的关键字;相反,当不声明为`explicit`时,默认允许这种隐含转换发生。这意味着如果构造函数未被标记为`explicit`,那么它可以参与自动类型转换过程[^2]。 #### Scala 相比之下,在Scala中`implicit`是一个有效并广泛使用的特性。它主要用于两个方面——隐式转换与隐式参数。前者是指通过定义带有一个参数的方法来进行类型的自动转换;后者则是指那些可以在调用时不显式传递但在运行期间由编译器自动查找并应用的参数[^3]。 ```scala // 定义一个隐式转换函数 implicit def intToString(x: Int): String = "" + x // 使用隐式参数 def greet(implicit name: String) = println(s"Hello, $name") val greetingMessage = "World" greet // 编译器会寻找名为"greetingMessage"的隐式值并将其作为参数传入 ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Achilles.Wang

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值