使用右值的比较函数

时间:2018-07-02 22:14:00

标签: c++ tuples comparison rvalue-reference

这是尝试为类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_tuplestd::ref:0份,共2步
  • std::forward_as_tuple内联:0份,0步

3 个答案:

答案 0 :(得分:1)

您可以将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);
};

答案 1 :(得分:1)

编辑:重写的答案,我这次已经适当考虑了该问题(即使我的原始答案是正确的)。

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_A()返回的元组的lambda主体中调用make_comparison_object所创建的临时项,因此让我们运行一些测试并查看不同值的结果BEHAVIOUR

首先,BEHAVIOR = 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 =====

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

最后,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

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

正如我刚开始所说的那样,整个过程归结为不返回指向堆栈上对象的指针(或引用)。都被花哨的衣服包裹住了。

Live demo


更新-对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(或实际上是任何其他类型的函数)中构造的非原始临时文件(无论以何种方式),您就必须正确地执行它,并且曾经如此。 那是我试图在这里讲到的

答案 2 :(得分:-2)

我最终使用了“内联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==(只需将“宏”作用在两个函数周围)

  • 有趣地使用了std::forward_as_tuple:将“ std::tuple<Types&&...>::operator<的参数“转发”到INSERT INTO TABLE B SELECT * FROM TABLE A 以便其用于预定目的

缺点:

  • 宏观丑陋