Lambdas и std :: function, Modern C ++ - PullRequest
2 голосов
/ 29 марта 2019

У меня есть класс, который применяет некоторые вычисления к данным.В этом случае трансформация.Однако классу не важно, что представляют собой вычисления преобразования или какие параметры ему требуются (он знает только, что это должно быть сделано для вектора целых чисел).Функция-член compute () выполняет все вычисления.

class Transform {
  public:
    Transform();
    void compute();
    void print();
  private:
    std::vector<int> data;
};

В старом стиле C ++ я бы предположил, что правильное, что нужно сделать, это наследовать от класса Transform (и сделать вычисления виртуальными) и реализовать все пользовательскиевычислять функции с их конкретными аргументами и перегружать их.

class TransformType1 : public Transform {
  public:
    Transform();
    void compute();
    void compute(int max_int, int min_int); //Truncates all the values outside of range
    void print();
  private:
    std::vector<int> data;
};

Это было бы очень хорошо сделать с лямбда-выражением и std::transform с данным вектором, так почему бы не сделать то же самое внутри класса?

#include <algorithm>
#include <functional>
#include <iostream>
#include <vector>

// Generic class, used for other stuff does not care about the specifics
// of the parameters used in the compute lambda

class Transform {
 public:
  Transform();
  void compute();
  // This std::function will be set though the constructor in the future and
  // will be private
  std::function<int(int)> t_lambda;
  void print();

 private:
  std::vector<int> data;
};

Transform::Transform() : data({1, 3, 4, 5, 2, 1, 4, 5, 3, 5, 3, 6, 4, 6, 12}) {}

// Apply the transform given the function object
void Transform::compute() {
  std::transform(data.begin(), data.end(), data.begin(), t_lambda);
}

void Transform::print() {
  for (auto& e : data) {
    std::cout << e << "  ";
  }
  std::cout << std::endl;
}

int main() {
  Transform f;
  int max_num = 2; // External parameters 

  // The important part: lambda parameters are taken with the capture feature
  f.t_lambda = std::function<int(int)>([max_num](int x) -> int {
    // Truncate values above a threshold
    return x > max_num ? max_num : x;
  });

  f.compute();
  f.print();
}

Теперь я могу просто определить правила функции вычисления вне класса (как и предполагалось), и я могу оставить тот же тип (без шаблонов наследования) и даже добавить больше параметров (скорее, констант) в функцию лямбда-выражениябез перегрузки compute ().

В прошлом я не видел много подобного кода, поэтому не уверен, хорошая это идея или нет.Какие еще шаблоны я могу использовать в этой ситуации?

Ответы [ 2 ]

1 голос
/ 29 марта 2019

Я не думаю, что это хорошая идея.Основная проблема, которую я вижу здесь, заключается в том, что использование std::function почти наверняка предотвратит встраивание, что является весьма важным для производительности алгоритма std ::.Сравните ваш подход с этим .Первый пример - это упрощенная версия вашего кода сверху.Во второй версии я передаю лямбду Transform::compute напрямую, а не через std::function.Если вы посмотрите на сгенерированный код, то увидите, что во второй версии компилятор может в основном встроить все.В первой версии, с другой стороны, компилятор не может ничего встроить.Каждая итерация функции вычисления будет выполнять косвенный вызов лямбда-выражения, содержащегося в std::function.

. Помимо этих проблем с производительностью, мне неясно, что именно представляет собой такой класс Transform.Он связывает контейнер входных данных с помощью метода std::function и print(), что кажется мне совершенно произвольным скоплением несвязанных функциональных возможностей.Концептуально я бы рассматривал преобразование как нечто, что можно применить к некоторым входным данным для получения некоторых выходных данных.Данные, к которым применяется преобразование, кажутся чем-то довольно независимым от самого преобразования.Акт печати вектора на std::cout тем более.Нет ничего плохого в инкапсуляции определенного вида преобразования в классе.Но я бы сказал, что вы хотите, чтобы экземпляры этого класса Transform представляли конкретное преобразование, которое затем можно применить к любым заданным входным данным. Например, :

#include <algorithm>
#include <vector>

class MyTransform
{
    int max_num;

public:
    MyTransform(int max_num = 2) : max_num(max_num) {}

    void operator ()(std::vector<int>& data) const
    {
        std::transform(std::begin(data), std::end(data), std::begin(data), [this](int x)
        {
            return x > max_num ? max_num : x;
        });
    }
};

int main() {
  std::vector<int> data = {1, 3, 4, 5, 2, 1, 4, 5, 3, 5, 3, 6, 4, 6, 12};

  MyTransform f(2);

  f(data);
}

Здесь MyTransform фактически представляет экземпляр вашего примера преобразования (для некоторого параметра max_num).MyTransform объекты могут быть переданы и применены к любому данному std::vector<int>.Или сокращенная версия :

#include <algorithm>
#include <vector>

auto my_transform(int max_num)
{
    return [max_num](auto&& data)
    {
        std::transform(std::begin(data), std::end(data), std::begin(data), [max_num](int x)
        {
            return x > max_num ? max_num : x;
        });
    };
}

int main() {
  std::vector<int> data = {1, 3, 4, 5, 2, 1, 4, 5, 3, 5, 3, 6, 4, 6, 12};

  auto f = my_transform(2);

  f(data);
}
0 голосов
/ 29 марта 2019

С точки зрения валидности, а также простоты, да - это совершенно верный способ сделать что-то и очищает большую часть полиморфного кода, который вам пришлось бы писать (как вы упоминали для более старых версий C ++). Он абсолютно безопасен, универсален и имеет обширный стандартизированный интерфейс.

Однако, с точки зрения производительности, std::function считается плохим, потому что это полиморфный адаптер и имеет все накладные расходы, связанные с этим. Из-за этого, если вам нужна максимальная скорость, вам нужно использовать обычные функции, функции уровня пространства имен или статические функции-члены. В этом случае вы можете рассмотреть идиому пакета параметров void*.

Но помните, это только если ваша программа уже работает и вы просто хотите оптимизировать ее по скорости:

// holds extra args for do_something_1()
struct pack_1
{
    int    something;
    double something_else;
};
// holds extra args for do_something_2()
struct pack_2
{
    float       cool_stuff;
    std::string another_extra_param;
};

// expects _extra to be of type pack_1
void do_something_1(int a, int b, void *_extra)
{
    pack_1 *extra = (pack_1*)_extra;
    ...
}
// expects _extra to be of type pack_2
void do_something_2(int a, int b, void *_extra)
{
    pack_2 *extra = (pack_2*)_extra;
    ...
}

Затем вы можете использовать чистые указатели: void (*)(int, int, void*) для используемой функции и void* для дополнительных параметров для ее передачи. Вам просто нужно быть осторожным и убедиться, что вы передали правильный тип пакета дополнительных параметров.

...