为什么std::max返回const&?

5
我希望找到最大的Foo,并在其上调用一个非常量方法inc()。当然,在查找最大值时,我不想创建任何副本或移动对象,即我不想使用Foo foo = std::max(foo1, foo2)。我尝试编写自己的max函数,但g++坚持要求我返回const&。
#include <iostream>

class Foo
{
public:
  Foo(int x) : x_(x) { std::cout << "const" << std::endl; }
  Foo(const Foo& foo) : x_(foo.x_) { std::cout << "copy const" << std::endl; }
  Foo(Foo&& foo) : x_(foo.x_) { std::cout << "move const" << std::endl; }
  bool operator< (const Foo& foo) const { return x_ < foo.x_; }
  bool operator> (const Foo& foo) const { return x_ > foo.x_; }
  void inc() { ++x_; }
  int x_;
};

/*
 * Doesn't compile.  Must return const T& or must accept non-const T&
 *
template<typename T>
inline T& my_max(const T& f1, const T& f2)
{
  return f1 > f2 ? f1 : f2;
}
*
*/

int main()
{
  Foo foo1(6);      
  Foo foo2(7);      
  Foo& foo = std::max(foo1, foo2); //Doesn't compile.  Must be const Foo&. But then next line fails
  foo.inc();
  std::cout << foo.x_ << std::endl;
  return 0;
}

2
它返回一个const引用以避免不必要的复制,因为它早于右值引用和移动语义。如果今天创建它,它可能会被定义得不同,例如:http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2007/n2199.html - Jonathan Wakely
另一个选项是添加 Foo copy() const& { return *this; },这样你就可以写成 std::max(fooA,fooB).copy().inc();。有时候没有名称的复制构造函数会让人感到痛苦。 - MSalters
1
你已经知道一个答案:你的评论中写道“...或者必须接受非const T&...”。你的两个变量foo1foo2都是非const lvalue,而你想要用来存储最大值的变量Foo&foo也是非const lvalue。所以为什么要在任何地方引入const呢?从你的max中省略const即可——虽然也许你可以将其重命名为max_lvalue_ref以明确其功能。 - Aaron McDaid
@AaronMcDaid 我想传递const以通知编译器/程序员它们不会更改(想象一下如果我们正在处理更复杂的函数)。 - Agrim Pathak
@AgrimPathak。“他们不会改变”。你的意思是max不会试图改变它们? - Aaron McDaid
这可能会适得其反。如果max以const方式获取它们,那么编译器可能会假设foo1foo2在程序中任何地方都不能被修改。它们唯一被引用的时候是通过非const引用,因此这似乎是一个合理的假设。编译器可能会进行不正确的优化,基本上假设foo1foo2在程序中没有被修改。因此,如果你在最后打印foo1foo2的值,编译器可能会打印旧的(未递增)值。也许我有点夸张,但我怀疑这种不正确的假设是由优化所允许的。 - Aaron McDaid
2个回答

2

您有两个问题:

  1. 结果中缺少const限定符
  2. 返回对const引用参数的引用是危险的

在这种情况下:

Foo& foo = std::max(Foo(6), Foo(7));

编译器会在函数调用前构造临时对象,并在函数调用后销毁它们 - 因此你最终会得到对垃圾的引用。当然,如果你总是使用现有对象,那么它会正常工作 - 但是很容易忘记这种限制。
你可以从参数中删除const,这将解决两个问题,并且因为你打算修改对象,所以这应该是可以接受的。

std::max 返回 const&。你好像错过了问题? - Yakk - Adam Nevraumont
目前为止,有一个没有使用const参数的答案可以满足OP的需求,这就是为什么我没有提到它 - 如果OP不使用C++11,这仍然是一个很好的解决方案。是的,std::max返回const&并且也很危险:const Foo& f1 = std::max(Foo(1), Foo(2)); const Foo& f2 = std::max(Foo(3), Foo(4)); 在第二个操作符之后,f1.f变成了4。由于OP提到了类,并且我不知道为什么我们不能使用&而不是const&,所以我想警告一下这个问题。 - ISanych

2
template<typename T>
T my_max(T&& f1, T&& f2) {
  return std::forward<T>(f1 > f2 ? f1 : f2);
}

以上代码比较稳健,能够满足你的需求。需要注意的是,两个参数必须拥有相同的左值、右值或常量性,而std::max则没有这个要求。 这就是为什么max使用const&的原因。
虽然也可以写一个更复杂的版本来找到公共引用类别,但其表现可能会出乎意料。
因此,在返回值中缺少&并不代表它不会返回引用。在你的用例中,以上代码会返回引用。如果传递的是右值,它将返回一个值。
下面是试图编写一个super_max的尝试,如果传递相同类型的左值,则返回一个左值,如果传递两种不同类型的值或右值,则返回一个副本:
template<class A, class B>
struct max_return:std::common_type<A,B>{};
template<class A>
struct max_return<A&,A&>{
  using type=A&;
};
template<class A, class B>
using max_return_t = typename max_return<A,B>::type;

template<class T, class U>
max_return_t<T,U> super_max(T&& t, U&& u) {
  if (t < u)
    return std::forward<U>(u);
  else
    return std::forward<T>(t);
}

这段代码仅使用了 <,并且更喜欢在绑定时使用左侧。

实例


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