使用已移动的变量值错误

5

我已经开始使用C++11,现在正在系统地通过值传递字符串到我的构造函数中。但是现在我意识到,如果在构造函数体中也使用该值,那么很容易引入错误:

class A(std::string val):
  _val(std::move(val))
{
  std::cout << val << std::endl; // Bug!!!
}

我该怎么做才能降低出错的几率?


7
"made the move to C++11" 非常好。 - Lightness Races in Orbit
3
他忘了。 - Lightness Races in Orbit
3
我正在写答案的过程中,但归根结底,答案就是简单明了的:“一开始不要搞砸,写单元测试。” - John Dibling
1
@JohnDibling,所以单元测试不一定能捕捉到这种情况。也就是说,你简化的答案并不是一个好答案。 - Arne Mertz
2
C++需要线性类型! - R. Martinho Fernandes
显示剩余10条评论
2个回答

2

在构造函数的实现中,指定其目的是以某种独特的方式被移动的参数名称

A::A(std::string val_moved_from):
 _val(std::move(val_moved_from))
{
  std::cout << val_moved_from << std::endl; // Bug, but obvious
}

尽早离开那些对象(在构造列表中)。

如果您的构造列表很长,可能会错过其中两个val_moved_from的用法,这并没有帮助。

另一种解决方法是撰写一个提案来解决这个问题。比如,扩展C++,使得本地变量的类型或作用域可以通过对它们进行操作而更改,因此std::safe_move(X)既从X中移动,又将X标记为过期变量,在其作用域的其余部分中不再有效。解决当变量半过期(在一个分支中过期,但在另一个分支中未过期)时该怎么做是一个有趣的问题。

因为这是疯狂的,我们可以将其视为库问题而不是语言问题。在一定程度上,我们可以在运行时模拟这些技巧(变量类型更改),这很粗略,但给出了思路:

template<typename T>
struct read_once : std::tr2::optional<T> {
  template<typename U, typename=typename std::enable_if<std::is_convertible<U&&, T>::value>::type>
  read_once( U&& u ):std::tr2::optional<T>(std::forward<U>(u)) {}
  T move() && {
    Assert( *this );
    T retval = std::move(**this);
    *this = std::tr2::none_t;
    return retval;
  }
  // block operator*?
};

请编写一个线性类型,只能通过 move 读取,读取后如果有错误则会抛出 Assert

然后修改你的构造函数:

A::A( read_once<std::string> val ):
  _val( val.move() )
{
  std::cout << val << std::endl; // does not compile
  std::cout << val.move() << std::endl; // compiles, but asserts or throws
}

通过使用转发构造函数,你可以提供一个更为简单的接口,避免使用 `read_once` 类型,然后在参数周围添加 `read_once<>` 封装,将其转发到你的“安全”(可能是 `private`)版本的构造函数中。
如果你的测试覆盖了所有的代码路径,即使你从 `read_once` 变量中进行多次 `move` 操作,你也会获得漂亮的 `Assert`,而不仅仅是空的 `std::string`。

2
但是你必须记得做所有这些。如果你能记住,那么你很可能在第一次就不会犯错。 - Lightness Races in Orbit
@LightnessRacesinOrbit 当然可以:但是如果你有一个强大的read_once,并遵循“始终将'按值复制'作为read_once”的规则,那么当你读取它时,你会得到Assert而不是空数据。而且哨子确实能够防止老虎靠近,你看到有老虎吗? - Yakk - Adam Nevraumont
如果你遵循了“不要尝试打印错误的变量”的规则,那么这一切都将无意义。 - Lightness Races in Orbit

0
“已经转移到C++11,现在我正在系统地通过值传递我的字符串到构造函数。”
“很抱歉,我不明白为什么要这样做。相对于传统方法,这种方法提供了什么改进?(基本上是防错的)”
“类A(const std::string& s):_val(s){std :: cout << s << std :: endl; //没有错误! std :: cout << _val << std :: endl; //也没有错误!!!”

回答你的问题:是的,std::move 比这个更好,因为它减少了拷贝。而且,你的回答并没有回答问题。 - milleniumbug
假设你的字符串 s 是一个长度为 100,000 的字符。你的版本总是会在这里复制它:_val(s)。但是上面的版本如果调用者对构造函数使用了 std::move 或使用一个临时 std::string 构建了 A,则不会复制它。参见这里获取更深入的讨论。请注意,拥有 std::string&& sstd::string const& s 构造函数比只取 std::string s 少移动一次,但需要维护两倍的代码量。 - Yakk - Adam Nevraumont
在这种情况下,我们根本不会进行任何复制,因为我们是通过引用传递的。我非常熟悉所引用的讨论。它解决了一种存在复制的情况,而这里并非如此。所提出的问题是“我该怎么做才能减少出错的可能性?”答案是“不要那样做!”这样做并没有减少任何副本,反而引入了一个错误。 - Robert Ramey

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接