Функция сравнения с использованием значений r - PullRequest
0 голосов
/ 03 июля 2018

Вот попытка сделать собственный компаратор для класса 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 не продлевает время жизни тех значений, на которые они указывают; они будут уничтожены в конце лямбды. Поэтому return make_comparison_object(lhs) < make_comparison_object(rhs); будет обращаться к висящим ссылкам и вызывать неопределенное поведение.

Я вижу два пути решения этой проблемы:

  • Используйте std::make_tuple вместо std::forward_as_tuple. Это будет работать, но я обеспокоен тем, что это может привести к дополнительным копиям или перемещениям, в частности, я думаю, что это может скопировать любые l-значения, переданные в std::make_tuple, такие как foo.s.

  • Вставьте содержимое лямбды, как это:

    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::make_tuple: 2 копии, 2 хода
  • std::make_tuple с std::ref: 0 копий, 2 хода
  • std::forward_as_tuple встроенный: 0 копий, 0 ходов

Ответы [ 3 ]

0 голосов
/ 03 июля 2018

Редактировать: Переписанный ответ, на этот раз я рассмотрел проблему должным образом (хотя мой первоначальный ответ был правильным).

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() в теле лямбда-выражения в кортеже, возвращаемом make_comparison_object, поэтому давайте запустим несколько тестов и посмотрим на результаты для других значений 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 =====

Тот же результат, который меня совсем не удивляет. В конце концов, мы вернули ссылку на переменную в стеке.

И, наконец, ПОВЕДЕНИЕ = 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. Все, что он делает - это делает ссылку на копию. Это еще свисающий указатель под скином, если сама ссылка исчезнет, ​​пока вы все еще используете оболочку, как здесь.

И, как я сказал в начале, все сводится к тому, что мы не возвращаем указатель (или ссылку) на объект в стеке. Это просто завернутые в модную одежду.

Живая демоверсия .


Обновление - лучший анализ оригинального поста ОП.

Давайте посмотрим, что на самом деле ОП помещает в свой кортеж:

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 исходит из параметра, переданного в лямбду, так что все в порядке
  • -foo.x - это примитивный тип, так что это тоже нормально
  • с кодом, как написано, std::accumulate вернуть int, так что снова мы в порядке
  • std::make_optional создает временное, поэтому это , а не безопасно

Таким образом, этот код на самом деле небезопасен, но не по той причине, что указывает OP, и ответ @ xskxzr фактически ничего не дает. Как только вы захотите экспортировать не примитивный временный объект, созданный внутри лямбда-функции (или вообще любой другой тип функции) - какими бы то ни было средствами - вы должны сделать это правильно, и так было всегда. Это , что я пытаюсь донести до нас.

0 голосов
/ 08 июля 2018

В итоге я использовал метод «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 в нужных местах

  • является безопасным, поскольку ссылки на rvalue используются до конца полного выражения

  • часто можно использовать для определения operator< и operator== за один раз (просто "охватите" макрос вокруг обеих функций "

  • интересным образом использует std::forward_as_tuple: он "пересылает" аргументы в std::tuple<Types&&...>::operator<, поэтому он (вроде) используется по прямому назначению

Недостатки:

  • макросы некрасивые
0 голосов
/ 03 июля 2018

Вы можете использовать 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);
};
...