使用右值的比较函数
Comparison function using rvalues
这是为 class 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
的任何左值,例如 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 << "constructor\n"; }
A(const A& other) : data(other.data) { std::cout << "copy constructor\n"; }
A(A&& other) : data(other.data) { std::cout << "move constructor\n"; }
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_tuple
与 std::ref
:0 份,2 次移动
std::forward_as_tuple
内联:0 份,0 次移动
您可以将 std::make_tuple
与 std::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 看在上帝的份上,不要 return 从函数或方法中指向基于堆栈的变量的指针或引用,无论代码多么花哨看。这基本上就是这个问题的全部内容。
让我们从一个测试程序开始,在我看来,它构成了一个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
编辑的元组 return 中的 lambda 主体内调用 make_A()
创建的临时文件,所以让我们 运行一些测试并查看 BEHAVIOUR
.
不同值的结果
首先,行为 = USE_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)
===== comparison end, result: 1 =====
这样就成功了,没有多余的副本(虽然有一些动作,但你需要那些)。
现在让我们试试 BEHAVIOR = 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 =====
同样的结果,我一点也不惊讶。毕竟,我们 return 引用了堆栈上的变量。
最后,BEHAVIOR = 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
,正如我最初 post 编辑的那样。
请注意,使用 std::ref
时必须格外小心。 它所做的一切 是使引用可复制。如果在您仍在使用包装器时引用本身消失了,它仍然是皮肤下的悬空指针,就像它在这里所做的那样。
正如我在开头所说的,这整个事情归结为不 return 指向堆栈上对象的指针(或引用)。只是全都裹在华丽的衣服里。
更新 - 更好地分析 OP 的原始 post。
让我们看看 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
return 和 int
,所以我们再次确定
std::make_optional
构造一个临时的,所以 那是 不 安全的
因此,该代码实际上并不安全,但并非出于 OP 声明的原因,@xskxzr 的回答实际上没有任何贡献。一旦您想导出在 lambda(或实际上任何其他类型的函数)内构造的非原始临时函数 - 无论以何种方式 - 您都必须正确地做,并且曾经如此。 这就是我想要表达的意思。
我最终使用了 "inline std::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==
(只是"scope"围绕这两个函数的宏
有趣地使用了 std::forward_as_tuple
:它 "forwards" 是 std::tuple<Types&&...>::operator<
的参数,因此它(有点)用于其预期目的
缺点:
- 宏丑陋
这是为 class 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
的任何左值,例如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 << "constructor\n"; }
A(const A& other) : data(other.data) { std::cout << "copy constructor\n"; }
A(A&& other) : data(other.data) { std::cout << "move constructor\n"; }
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_tuple
与std::ref
:0 份,2 次移动std::forward_as_tuple
内联:0 份,0 次移动
您可以将 std::make_tuple
与 std::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 看在上帝的份上,不要 return 从函数或方法中指向基于堆栈的变量的指针或引用,无论代码多么花哨看。这基本上就是这个问题的全部内容。
让我们从一个测试程序开始,在我看来,它构成了一个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
编辑的元组 return 中的 lambda 主体内调用 make_A()
创建的临时文件,所以让我们 运行一些测试并查看 BEHAVIOUR
.
首先,行为 = USE_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)
===== comparison end, result: 1 =====
这样就成功了,没有多余的副本(虽然有一些动作,但你需要那些)。
现在让我们试试 BEHAVIOR = 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 =====
同样的结果,我一点也不惊讶。毕竟,我们 return 引用了堆栈上的变量。
最后,BEHAVIOR = 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
,正如我最初 post 编辑的那样。
请注意,使用 std::ref
时必须格外小心。 它所做的一切 是使引用可复制。如果在您仍在使用包装器时引用本身消失了,它仍然是皮肤下的悬空指针,就像它在这里所做的那样。
正如我在开头所说的,这整个事情归结为不 return 指向堆栈上对象的指针(或引用)。只是全都裹在华丽的衣服里。
更新 - 更好地分析 OP 的原始 post。
让我们看看 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
return 和int
,所以我们再次确定 std::make_optional
构造一个临时的,所以 那是 不 安全的
因此,该代码实际上并不安全,但并非出于 OP 声明的原因,@xskxzr 的回答实际上没有任何贡献。一旦您想导出在 lambda(或实际上任何其他类型的函数)内构造的非原始临时函数 - 无论以何种方式 - 您都必须正确地做,并且曾经如此。 这就是我想要表达的意思。
我最终使用了 "inline std::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==
(只是"scope"围绕这两个函数的宏有趣地使用了
std::forward_as_tuple
:它 "forwards" 是std::tuple<Types&&...>::operator<
的参数,因此它(有点)用于其预期目的
缺点:
- 宏丑陋