代码之家  ›  专栏  ›  技术社区  ›  jcai

使用右值的比较函数

  •  2
  • jcai  · 技术社区  · 8 年前

    下面是一个为类创建自定义比较器的尝试 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);
        }
    };
    

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

    我可以从两个方面来看:

    • 使用 std::make_tuple 而不是 std::forward_as_tuple 是的。这将起作用,但我担心它可能会导致额外的复制或移动,特别是我认为它可能会复制传递给 std::创建元组 例如 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);
      

    这也行,但它看起来很可怕,违反干燥。

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

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

    #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::创建元组 :2个副本,2个移动
    • std::创建元组 具有 std::ref :0个副本,2个移动
    • std::转发 内联:0个副本,0个移动
    3 回复  |  直到 8 年前
        1
  •  1
  •   xskxzr    8 年前

    你可以用 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);
    };
    
        2
  •  1
  •   catnip    7 年前

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

    TL;博士 看在老天的份上,不要从函数或方法返回指向基于堆栈的变量的指针或引用,不管代码看起来有多花哨。本质上,这就是问题的全部。

    让我们从一个测试程序开始,在我看来,它构成了 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 是的。

    首先,行为= 使用元组 以下内容:

    ===== 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 =====
    

    所以这是有效的,没有额外的副本(虽然有一些移动,但你需要这些)。

    现在让我们试试行为= 向前使用标准 以下内容:

    ===== 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 =====
    

    如你所见,这是一场灾难,当我们试图接近他们的时候,他们已经不在了。我们继续吧。

    现在的行为= 使用标准参考 以下内容:

    ===比较开始=====
    ===构造元组=====
    从2,3比较对象
    构造函数(-3)
    A损坏(-3)
    从2,1比较对象
    构造函数(-1)
    A损坏(-1)
    ===检查/比较元组=====
    左侧元组<0>=0,右侧元组<0>=0<=不正常
    ===比较结束,结果:0=====
    

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

    最后,行为= 使用标准移动 是的。如你所见,结果和打电话一样 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 是的。


    更新 -更好地分析了这篇文章的原稿。

    让我们看看操作人员到底在元组中放了什么:

    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(或者任何其他类型的函数)中构造的非本原临时函数——不管用什么方法——你就必须正确地执行它,而且它曾经是这样的。 那是 我想说的是。

        3
  •  -2
  •   jcai    7 年前

    我最后用了“内联 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 在正确的地方

    • 是安全的,因为在完整表达式结束之前使用了rvalue引用

    • 通常可以用来定义 operator< operator== 一次性(只需“范围”两个函数周围的宏)

    • 有趣地利用 std::转发 :它将参数“转发”到 std::tuple<Types&&...>::operator< 所以它(有点)被用于它的目的

    缺点:

    • 宏丑陋