有没有一个与`std::move`相反效果的转换(或标准函数)?

6
首先,这个问题不是Function dual to std::move?Does the inverse of std::move exist?的重复。我不是在询问防止移动在本应发生时进行复制的机制。相反,我在询问一种机制,使可修改的左值引用的位置接受rvalue。实际上,这与std::move被发明的情况恰好相反(即使可修改的左值被接受到将要绑定到(可修改的)rvalue引用的位置)。
在我的情况下,rvalue将不被接受,因为上下文需要一个可修改的左值引用。由于某种我不太理解但愿意接受的原因,(可修改的)rvalue表达式将绑定到常数左值引用(而不引入额外的临时变量),但它不会绑定到可修改的左值引用(gcc给出的错误消息是“invalid initialization of non-const reference of type ‘A&’ from an rvalue of type ‘A’ ”,而clang则表示“non-const lvalue reference to type 'A' cannot bind to a temporary of type 'A' ”;奇怪的是,我无法让这两个编译器承认所讨论的表达式具有类型“A&&”,即使该表达式实际上是static_cast<A&&>(...)这样的形式,它本身不会引发错误)。我可以理解,通常不希望在需要可修改的左值引用的位置接受rvalue表达式,因为它意味着通过该左值引用进行的任何修改都将丢失,但正如调用std::move表示对编译器说“我知道这是一个将要绑定到rvalue引用(参数)的左值,因此可能会被窃取,但我知道我在做什么,在这里是可以的”,我想在我的情况下说“我知道这是一个将要绑定到可修改的左值引用(参数)的临时对象,因此通过左值引用进行的任何更改都将不会被注意到,但我知道我在做什么,在这里是可以的”。
我可以通过从rvalue初始化类型为A的命名对象来解决问题,然后在需要可修改的左值引用的地方提供名称。我认为这不会增加任何额外的运行时开销(rvalue需要一个临时变量),但是必须以几种方式解决这个问题:必须引入一个虚拟名称,也许必须引入一个复合语句来保存声明,将产生rvalue的表达式与提供其参数的函数调用分开。因此,我的问题是是否可以在不引入虚拟名称的情况下完成这项工作:
  1. 是否有任何方法(例如使用强制类型转换),可以将类型为A的rvalue表达式绑定到类型为A&的可修改lvalue引用,而不引入类型为A的命名对象?
  2. 如果没有,这是一个故意的选择吗?(如果是,为什么?)如果有,是否提供了类似于标准中提供的std::move机制来促进它?

这里有一个简化的示例,我需要这样的转换。我故意删除了A的特殊构造函数,以确保错误消息不涉及编译器决定引入的临时变量。当A&被替换为const A&时,所有错误都消失了。

class A
{ int n;
public:
  A(int n) : n(n) {}
  A(const A&) = delete; // no copying
  A(const A&&) = delete; // no moving either
  int value() const { return n; }
};

int f(A& x) { return x.value(); }

void g()
{ A& aref0 = A(4); // error
  // exact same error with "= static_cast<A&&>(A(4))" instead of A(4)
  A& aref1 = static_cast<A&>(A(5)); // error
  // exact same error with "= static_cast<A&&>(A(5))" instead of A(5)
  f (A(6)); //error
  // exact same error with "= static_cast<A&&>(A(6))" instead of A(6)

  A a(7);
  f(a); // this works
  A& aref2 = a; // this works too, of course
}

对于那些想知道为什么我需要这个的人,这里有一个用例。我有一个带有参数的函数f作为输入参数,有时也作为输出参数,把提供的值替换为“更专业”的值(该值表示树形结构,并且一些缺失的分支可能已经填充了);因此将此值传递为可修改的lvalue引用。我还有一些全局变量,保存的值有时用于为此参数提供值;这些值是不可改变的,因为它们已经完全专业化。尽管如此,我过去没有声明这些变量为const,因为这样会使它们不适用于参数。但是它们确实被假定为全局和永久常量,因此我想重新编写我的代码,以使这一点明确,并避免在更改f的实现时意外出错(例如,当抛出异常时,它可能决定从其参数中移动;当参数表示将被异常销毁的局部变量时,这将是可以接受的,但如果绑定到全局“常量”上,则会导致灾难)。因此,每当将这些全局常量之一传递给f时,我决定制作一份副本。有一个函数copy,它制作并返回这样的拷贝,我想将调用它作为参数传递给f;遗憾的是,由于copy(c)是一个rvalue,因此出于上述原因无法这样做,尽管这种用法是完全安全的,实际上比我的先前解决方案更安全。

3
提示:如果你创建了一个使用右值引用的函数,在函数内部该参数的名称就是左值。 - R. Martinho Fernandes
@R.MartinhoFernandes 我知道这一点,但这将会给 f 的调用者完全错误的信号。使用右值引用意味着“提供一个值,并期望它被窃取并降为无用的垃圾”。但实际上,f 通常通过其参数导出信息,通过特化其值;只有在提供那些全局常量的罕见情况下,我才知道不会有额外的信息出现。此外,采用右值引用将使我不得不在所有那些本地变量作为参数的正常情况下插入 std::move - Marc van Leeuwen
感谢@dyp指出这一点;至少解释了错误消息的语言。对我来说有点困惑的是,static_cast用于更改表达式的类型,但根据您引用的内容,xstd::move(x)的类型完全相同(一旦引用被丢弃),因此在std::move中的转换没有改变类型,而是改变了值类别。但我可以接受这种困惑。 - Marc van Leeuwen
1
@dyp 它应该被适当地重新设计,只指向汽车:一个可以编译auto_ptr<boat>的语言显然是一种可憎的东西。 - Casey
你的缩进风格很糟糕。 - Lightness Races in Orbit
显示剩余6条评论
4个回答

9
最简单的解决方法是这样的:
template<typename T>
T& force(T&& t){
   return t;
}

7
下面提供的函数是一个不好的想法。不要使用它。它会导致悬空引用问题,这是一个非常容易出现的问题。如果需要使用该代码,请认真检查并进行必要的修复。
尽管如此,我认为这是一个有趣的练习,所以我还是想展示一下。
在函数内部,参数的名称是左值(lvalue),即使参数是右值引用(rvalue references)。您可以利用这个属性将参数作为左值引用返回。
template <typename T>
constexpr T& as_lvalue(T&& t) {
    return t;
};

你的函数接受通用引用,不仅限于右值引用。这样更好...顺便说一下:std::remove_reference 是多余的。 - Deduplicator
@Deduplicator 是的,我只是从 std::move 中偷了签名 :) - R. Martinho Fernandes
就Deduplicator的回答而言,我不明白为什么你要对这个函数模板的潜在用途如此轻视。像std::move一样,它是为特定用途而设计的,如果随意独立使用,可能会产生灾难性的影响。我在问题的结尾描述了一个具体情况,在这种情况下,使用它似乎是合理的;你有什么反对这种特定用法的吗? - Marc van Leeuwen
事实上,如果您省略第一段,我会接受这个答案。as_lvalue 这个名称肯定是最好的选择,有趣的是一个函数(模板)可以做到强制转换所不能做到的。 - Marc van Leeuwen

2

这似乎适用于你所有的情况:

template <class T>
constexpr typename std::remove_reference<T>::type& copy(T&& t) {
    return t;
};

它与std::move非常相似,唯一不同的是它返回一个左值引用。


2
为什么要费心使用remove_reference呢? - Yakk - Adam Nevraumont
否则,将调用具有 T& 返回类型的模板实例中的 T& t。 - Solkar
我对模板推导逻辑的了解不足,无法看出为什么这个解决方案比gexicide提出的更优,但您选择的constexprforce_copy名称似乎表明您没有理解预期的用途。这不是强制复制的问题;复制将通过显式调用函数来完成(结果地址肯定不是编译时常量),但问题在于使结果被接受并绑定到T&的位置。 - Marc van Leeuwen
@MarcvanLeeuwen 我把它称为 force_copy,所以它有点像 std::move。我本来会称其为 copy,但那很容易与 std::copy 混淆。就像 std::move 并不实际移动对象一样,这个 copy 也并不实际复制。而且我已经将其设置为 constexpr,就像 std::move 在 C++14 中是 constexpr 一样。 - Joseph Mansfield
“但那很容易与std :: copy混淆”,你的意思是,就像std :: move(强制转换)和std :: move(算法)一样?;) - dyp
@dyp 我完全忘记了算法。老实说,这只是标准库中糟糕的命名。但是为了一致性,我也会将我的更改为copy - Joseph Mansfield

1

如果在转发函数中省略完美转发,那么你就得到了no_move

template<class T> constexpr T& no_move(T&& x) {return x;}

请确保假装您拥有左值引用而不是右值引用是可以的,因为这会规避绑定临时对象到非 const 引用的保护措施。

使用此函数的任何代码几乎肯定存在设计缺陷。

在您的示例用例中,正确的方法是将 f() 的参数类型更改为 const&,因此不需要这样的适配器,并使用 const_cast 将其添加到保存计算结果的缓存中。
但请确保不要更改声明为 const 的对象的非 mutable 成员。


1
你是在暗示我在问题末尾描述的函数f中的使用存在设计缺陷吗?如果是这样,那么这个缺陷是什么,应该如何修改设计? - Marc van Leeuwen
1
你是否读完了问题?f的部分功能是通过修改其参数来潜在地导出信息;如果参数是const&,则这是不可能的。(并且如果参数是那些“全局常量”之一的副本,则很容易证明不会发生导出情况)。 - Marc van Leeuwen
1
我认为你误解了我的例子;你添加的解释让事情变得更糟。f 的参数“必须是可修改的”,这是 f 规范的一部分。把 f 看作一个函数,它将一个模板(如 C++ 中的类模板,但在我的程序中编译的是另一种语言)映射到该模板的实例;这就是“添加分支”的部分。只有那些非常特殊的全局值不会改变,因为它们已经完全特化(没有模板参数)类型。没有缓存或可变部分涉及。 - Marc van Leeuwen
所以,实际上签名更像是 const instantiation& f(const my_template&),而 f 检查是否存在预先实例化的模板,否则会添加一个? - Deduplicator
1
不,可能的更改是通过调用my_template参数导出的,而不是作为返回值(有一个不同且不直接相关的返回值)。 请相信我,f函数的参数必须是可修改的(我想我以前说过这个)。 坚持让我把它设为“const”是没有意义的,如果我这样做,它就无法编译。 同时用于输入和输出的参数有其用途,这是其中之一。 稍微不寻常的是,对于某些输入,该值可以预测地不会被修改(但仍提供必要的信息)。 - Marc van Leeuwen
显示剩余3条评论

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