使用右值的比较函数

Comparison function using rvalues

本文关键字:比较 函数      更新时间:2023-10-16

下面尝试为类Foo制作一个自定义比较器。它将对成员应用一些转换,然后按字典进行比较:

struct Foo {
std::string s;
float x;
std::vector<int> z;
std::unique_ptr<std::deque<double>> p;
friend bool operator<(const Foo& lhs, const Foo& rhs) {
auto make_comparison_object = [](const Foo& foo) {
return std::forward_as_tuple(
foo.s,
-foo.x,
std::accumulate(
foo.z.begin(),
foo.z.end(),
0),
foo.p ? std::make_optional(*foo.p) : std::nullopt);
};
return make_comparison_object(lhs) < make_comparison_object(rhs);
}
};

虽然优雅,但这里有一个问题:右值引用,例如对-foo.x结果的引用,不能充分延长它们所指向的右值的寿命;它们将在lambda结束时被销毁。因此,return make_comparison_object(lhs) < make_comparison_object(rhs);将访问悬空引用并导致未定义的行为。

我可以看到两种方法:

  • 使用std::make_tuple而不是std::forward_as_tuple。这会起作用,但我担心它可能会产生额外的副本或移动,特别是我认为它可能会复制传递给std::make_tuple的任何lvalue,如foo.s

  • 内联lambda的内容,如下所示:

    return std::forward_as_tuple(
    lhs.s,
    -lhs.x,
    std::accumulate(
    lhs.z.begin(),
    lhs.z.end(),
    0),
    lhs.p ? std::make_optional(*lhs.p) : std::nullopt)
    < std::forward_as_tuple(
    rhs.s,
    -rhs.x,
    std::accumulate(
    rhs.z.begin(),
    rhs.z.end(),
    0),
    rhs.p ? std::make_optional(*rhs.p) : std::nullopt);
    

这也有效,但看起来很糟糕,违反了DRY。

有没有更好的方法来完成这种比较?

编辑:以下是一些测试代码,比较了建议的解决方案:

#include <functional>
#include <iostream>
#include <tuple>
#define BEHAVIOR 2
struct A {
A(int data) : data(data) { std::cout << "constructorn"; }
A(const A& other) : data(other.data) { std::cout << "copy constructorn"; }
A(A&& other) : data(other.data) { std::cout << "move constructorn"; }
friend bool operator<(const A& lhs, const A& rhs) {
return lhs.data < rhs.data;
}
int data;
};
A f(const A& a) {
return A{-a.data};
}
struct Foo {
Foo(A a1, A a2) : a1(std::move(a1)), a2(std::move(a2)) {}
A a1;
A a2;
friend bool operator<(const Foo& lhs, const Foo& rhs) {
#if BEHAVIOR == 0
auto make_comparison_object = [](const Foo& foo) {
return std::make_tuple(foo.a1, f(foo.a2));
};
return make_comparison_object(lhs) < make_comparison_object(rhs);
#elif BEHAVIOR == 1
auto make_comparison_object = [](const Foo& foo) {
return std::make_tuple(std::ref(foo.a1), f(foo.a2));
};
return make_comparison_object(lhs) < make_comparison_object(rhs);
#elif BEHAVIOR == 2
return std::forward_as_tuple(lhs.a1, f(lhs.a2))
< std::forward_as_tuple(rhs.a1, f(rhs.a2));
#endif
}
};
int main() {
Foo foo1(A{2}, A{3});
Foo foo2(A{2}, A{1});
std::cout << "===== comparison start =====n";
auto result = foo1 < foo2;
std::cout << "===== comparison end, result: " << result << " =====n";
}

你可以在Wandbox上试试。结果在gcc/clang上都是一致的,并且考虑到元组的构建是有意义的:

  • std::make_tuple:2份,2次移动
  • std::make_tuplestd::ref:0份,2次移动
  • std::forward_as_tuple内联:0个副本,0个移动

您可以将std::make_tuplestd::ref:一起使用

auto make_comparison_object = [](const Foo& foo) {
return std::make_tuple(
std::ref(foo.s),
// ^^^^^^^^
-foo.x,
std::accumulate(
foo.z.begin(),
foo.z.end(),
0),
foo.p ? std::make_optional(*foo.p) : std::nullopt);
};

编辑:重写答案,这次我已经适当考虑了这个问题(尽管我原来的答案是正确的)。

tl;dr看在上帝的份上,无论代码看起来多么花哨,都不要从函数或方法返回对基于堆栈的变量的指针或引用。从本质上讲,这就是这个问题的全部。

让我们从一个测试程序开始,在我看来,它构成了MCVE:

#include <iostream>
#include <functional>
#include <tuple>
#define USE_MAKE_TUPLE  0
#define USE_STD_FORWARD 2
#define USE_STD_REF     3
#define USE_STD_MOVE    4
#define BEHAVIOR        USE_STD_MOVE
struct A {
A(int data) : data(data) { std::cout << "A constructor (" << data << ")n"; }
A(const A& other) : data(other.data) { std::cout << "A copy constructor (" << data << ")n"; }
A(A&& other) : data(other.data) { std::cout << "A move constructor (" << data << ")n"; }
A(const A&& other) : data(other.data) { std::cout << "A const move constructor (" << data << ")n"; }
~A() { std::cout << "A destroyed (" << data << ")n"; data = 999; } 
friend bool operator<(const A& lhs, const A& rhs) {
return lhs.data < rhs.data;
}
int data;
};
struct Foo {
Foo(A a1, A a2) : a1(std::move(a1)), a2(std::move(a2)) {}
A a1;
A a2;
friend bool operator< (const Foo& lhs, const Foo& rhs)
{
auto make_comparison_object = [](const Foo& foo)
{
std::cout << "make_comparison_object from " << foo.a1.data << ", " << foo.a2.data << "n";
#if BEHAVIOR == USE_MAKE_TUPLE
return std::make_tuple (make_A (foo), 42);
#elif BEHAVIOR == USE_STD_FORWARD
return std::forward_as_tuple (make_A (foo), 42);
#elif BEHAVIOR == USE_STD_REF
A a = make_a (foo);
return std::make_tuple (std::ref (a), 42);
#elif BEHAVIOR == USE_STD_MOVE
return std::make_tuple (std::move (make_A (foo)), 42);
#endif
};
std::cout << "===== constructing tuples =====n";
auto lhs_tuple = make_comparison_object (lhs);
auto rhs_tuple = make_comparison_object (rhs);
std::cout << "===== checking / comparing tuples =====n";
std::cout << "lhs_tuple<0>=" << std::get <0> (lhs_tuple).data << ", rhs_tuple<0>=" << std::get <0> (rhs_tuple).data << "n";
return lhs_tuple < rhs_tuple;
}
static A make_A (const Foo& foo) { return A (-foo.a2.data); }
};
int main() {
Foo foo1(A{2}, A{3});
Foo foo2(A{2}, A{1});
std::cout << "===== comparison start =====n";
auto result = foo1 < foo2;
std::cout << "===== comparison end, result: " << result << " =====n";
}

现在的问题是在make_comparison_object返回的元组中的lambda主体中捕获通过调用make_A()创建的临时,所以让我们运行一些测试,看看BEHAVIOUR的不同值的结果。

首先,行为=USE_MAKE_UPLE:

===== constructing tuples =====
make_comparison_object from 2, 3
A constructor (-3)
A move constructor (-3)
A destroyed (-3)
make_comparison_object from 2, 1
A constructor (-1)
A move constructor (-1)
A destroyed (-1)
===== checking / comparing tuples =====
lhs_tuple<0>=-3, rhs_tuple<0>=-1          <= OK
A destroyed (-1)
A destroyed (-3)
===== comparison end, result: 1 =====

所以这很有效,而且没有额外的副本(尽管有一些动作,但你需要这些)。

现在让我们尝试BEHAVIOUR=USE_STD_FORWARD:

===== comparison start =====
===== constructing tuples =====
make_comparison_object from 2, 3
A constructor (-3)
A destroyed (-3)
make_comparison_object from 2, 1
A constructor (-1)
A destroyed (-1)
===== checking / comparing tuples =====
lhs_tuple<0>=0, rhs_tuple<0>=0            <= Not OK
===== comparison end, result: 0 =====

正如你所看到的,这是一场灾难,当我们试图访问它们时,临时性的东西已经不见了。让我们继续前进。

现在行为=USE_STD_REF:

===== comparison start =====
===== constructing tuples =====
make_comparison_object from 2, 3
A constructor (-3)
A destroyed (-3)
make_comparison_object from 2, 1
A constructor (-1)
A destroyed (-1)
===== checking / comparing tuples =====
lhs_tuple<0>=0, rhs_tuple<0>=0            <= Not OK
===== comparison end, result: 0 =====

同样的结果,我一点也不惊讶。毕竟,我们返回了对堆栈上一个变量的引用。

最后,BEHAVIOUR=USE_STD_MOVE。正如您所看到的,结果与只调用std::make_tuple而不移动相同——正如您在从临时构建对象时所期望的那样:

===== constructing tuples =====
make_comparison_object from 2, 3
A constructor (-3)
A move constructor (-3)
A destroyed (-3)
make_comparison_object from 2, 1
A constructor (-1)
A move constructor (-1)
A destroyed (-1)
===== checking / comparing tuples =====
lhs_tuple<0>=-3, rhs_tuple<0>=-1          <= OK
A destroyed (-1)
A destroyed (-3)

总之,正如我最初发布的那样,只需使用std_make_tuple即可。

请注意,使用std::ref时必须格外小心它所做的一切就是使引用具有可复制性。如果引用本身在您仍在使用包装器时消失,它仍然是皮肤下的一个悬空指针,就像这里所做的那样。

正如我在开头所说的,这整件事可以归结为不返回指向堆栈上对象的指针(或引用)。一切都裹在花哨的衣服里。

现场演示。


更新-更好地分析OP的原始帖子。

让我们看看OP在他的元组中实际放了什么:

auto make_comparison_object = [](const Foo& foo) {
return std::forward_as_tuple(
foo.s,
-foo.x,
std::accumulate(
foo.z.begin(),
foo.z.end(),
0),
foo.p ? std::make_optional(*foo.p) : std::nullopt);

那么,他在里面放什么?井:

  • foo.s来自传递到lambda中的参数,所以没关系
  • -foo.x是基元类型,所以也可以
  • 按照编写的代码,std::accumulate返回一个int,所以我们还是可以的
  • std::make_optional构造了一个临时的,所以这是不安全的

因此,该代码实际上并不安全,但不是因为OP声明的原因,@xskxzr的回答实际上没有任何贡献。只要你想导出一个在lambda(或者任何其他类型的函数)内部构造的非基元临时函数,不管用什么方法,你都必须正确地执行它,而且从来都是这样这就是我想表达的意思。

我最终使用了"inlinestd::forward_as_tuple"方法,但使用了一个使事情更干燥的宏:

friend bool operator<(const Foo& lhs, const Foo& rhs) {
#define X(foo)                                            
std::forward_as_tuple(                                  
(foo).s,                                            
-(foo).x,                                           
std::accumulate((foo).z.begin(), (foo).z.end(), 0), 
(foo).p ? std::make_optional(*(foo).p) : std::nullopt)
return X(lhs) < X(rhs);
#undef X
}

优点:

  • 不会产生任何不必要的副本,甚至不会移动

  • 不必担心在正确的位置写入std::ref

  • 是安全的,因为右值引用是在全表达式结束之前使用的

  • 通常可用于一次性定义operator<operator==(仅"范围"宏的两个函数">

  • 有趣地使用了std::forward_as_tuple:它将参数"转发"给std::tuple<Types&&...>::operator<,因此它(有点)用于其预期目的

缺点:

  • 宏丑陋