четверг, 3 июля 2025 г.

[prog.c++] Шаблоны против копипасты 11: теперь в сочетании с if constexpr

Был у меня код вроде вот такого ([[nodiscard]] и прочие элементы хорошего стиля убраны для упрощения):

class element_finder_t
{
public:
  search_result_t
  find_element(const search_conditions_t & conditions)
  {
    if(should_use_first_algorithm(conditions))
      return find_by_first_algorithm(conditions);
    else if(should_use_second_algorithm(conditions))
      return find_by_second_algorithm(conditions);
    ...
  }

private:
  search_result_t
  find_by_first_algorithm(const search_conditions_t & conditions)
  {
    ... // Какой-то код для поиска элемента в цикле.
    if(is_conditions_fulfilled(element_info, conditions))
      return element_info;
    ... // Какой-то код для подготовки следующей итерации.
  }

  search_result_t
  find_by_second_algorithm(const search_conditions_t & conditions)
  {
    ... // Какой-то код для поиска элемента в цикле.
    if(is_conditions_fulfilled(element_info, conditions))
      return element_info;
    ... // Какой-то код для подготовки следующей итерации.
  }
};

В один прекрасный день потребовалось кроме метода find_element добавить еще и метод find_element_with_postprocessing. Отличие нового метода от старого find_element в том, что когда элемент удовлетворяющий условиям поиска найден, то нужно сделать некую дополнительную постобработку. К сожалению, постобработка может привести к тому, что элемент перестанет удовлетворять условиям поиска. В таком случае нужно попытаться найти следующий подходящий элемент.

В лоб такое расширение element_finder_t можно было бы сделать так:

class element_finder_t
{
public:
  search_result_t
  find_element(const search_conditions_t & conditions)
  {
    if(should_use_first_algorithm(conditions))
      return find_by_first_algorithm(conditions);
    else if(should_use_second_algorithm(conditions))
      return find_by_second_algorithm(conditions);
    ...
  }

  // Новый метод, фактически повторяет логику старого.
  search_result_t
  find_element_with_postprocessing(
    const search_conditions_t & conditions,
    const processing_params_t & params)
  {
    if(should_use_first_algorithm(conditions))
      return find_by_first_algorithm_with_postprocessing(
        conditions, params);
    else if(should_use_second_algorithmconditions))
      return find_by_second_algorithm_with_postprocessing(
        conditions, params);
    ...
  }

private:
  search_result_t
  find_by_first_algorithm(const search_conditions_t & conditions)
  {
    ... // Какой-то код для поиска элемента в цикле.
    if(is_conditions_fulfilled(element_info, conditions))
      return element_info;
    ... // Какой-то код для подготовки следующей итерации.
  }

  search_result_t
  find_by_second_algorithm(const search_conditions_t & conditions)
  {
    ... // Какой-то код для поиска элемента в цикле.
    if(is_conditions_fulfilled(element_info, conditions))
      return element_info;
    ... // Какой-то код для подготовки следующей итерации.
  }

  search_result_t
  find_by_first_algorithm_with_postprocessing(
    const search_conditions_t & conditions,
    const processing_params_t & params)
  {
    ... // Какой-то код для поиска элемента в цикле.
    if(is_conditions_fulfilled(element_info, conditions))
    {
      auto updated_info = postprocess(element_info, params);
      if(is_conditions_fulfilled(updated_info, conditions))
        return updated_info;
    }
    ... // Какой-то код для подготовки следующей итерации.
  }

  search_result_t
  find_by_second_algorithm_with_postprocessing(
    const search_conditions_t & conditions,
    const processing_params_t & params)
  {
    ... // Какой-то код для поиска элемента в цикле.
    if(is_conditions_fulfilled(element_info, conditions))
    {
      auto updated_info = postprocess(element_info, params);
      if(is_conditions_fulfilled(updated_info, conditions))
        return updated_info;
    }
    ... // Какой-то код для подготовки следующей итерации.
  }
};

Т.е. приходится дублировать кучу кода из-за нескольких различающихся строчек где-то в глубине конкретных алгоритмов поиска. Что есть копипаста, а от копипасты следует избавляться.

Как?

С помощью шаблонов, конечно же! На самом деле можно и без шаблонов, но тема-то "шаблоны против копипасты" 😉

Поэтому делаем так:

class element_finder_t
{
  // Вспомогательный тип для случая, когда пост-процессинг делать не нужно.
  struct no_processing_params_t {};

public:
  search_result_t
  find_element(const search_conditions_t & conditions)
  {
    // Делегируем саму работу.
    return do_find_element(conditions, no_processing_params_t{});
  }

  // Новый метод.
  search_result_t
  find_element_with_postprocessing(
    const search_conditions_t & conditions,
    const processing_params_t & params)
  {
    // Делегируем саму работу туда же.
    return do_find_element(conditions, params);
  }

private:
  // Вот кто реально делает работу.
  template<typename OptProcessingParams>
  search_result_t
  do_find_element(
    const search_conditions_t & conditions,
    const OptProcessingParams & opt_processing_params)
  {
    if(should_use_first_algorithm(conditions))
      return find_by_first_algorithm(
        conditions, opt_processing_params);
    else if(should_use_second_algorithm(conditions))
      return find_by_second_algorithm(
        conditions, opt_processing_params);
    ...
  }

  // Методы для разных способов поиска стали шаблонами.
  template<typename OptProcessingParams>
  search_result_t
  find_by_first_algorithm(
    const search_conditions_t & conditions,
    const OptProcessingParams & opt_processing_params)
  {
    ... // Какой-то код для поиска элемента в цикле.
    if(is_conditions_fulfilled(element_info, conditions))
    {
      // Обработка ситуации с пост-процессингом.
      if constexpr(std::is_same_v<OptProcessingParams, processing_params_t>)
      {
        auto updated_info = postprocess(element_info, params);
        if(is_conditions_fulfilled(updated_info, conditions))
          return updated_info;
      }
      else
        return element_info;
    }
    ... // Какой-то код для подготовки следующей итерации.
  }

  template<typename OptProcessingParams>
  search_result_t
  find_by_second_algorithm(
    const search_conditions_t & conditions,
    const processing_params_t & opt_processing_params)
  {
    ... // Какой-то код для поиска элемента в цикле.
    if(is_conditions_fulfilled(element_info, conditions))
    {
      // Обработка ситуации с пост-процессингом.
      if constexpr(std::is_same_v<OptProcessingParams, processing_params_t>)
      {
        auto updated_info = postprocess(element_info, params);
        if(is_conditions_fulfilled(updated_info, conditions))
          return updated_info;
      }
      else
        return element_info;
    }
    ... // Какой-то код для подготовки следующей итерации.
  }
};

Т.е. всю работу вынесли в минимальное количество вспомогательных шаблонных методов. И фокус в том, что если пост-процессинг нужен, то во вспомогательные методы отдаются реальные параметры для него. Этот случай специально обрабатывается в конкретных алгоритмах через if constexpr.

Если же постпроцессинг не нужен, то передается ссылка на пустое значение no_processing_params_t и никаких действий по пост-процессингу не выполняется.

Тем самым удалось избежать дублирования основного кода алгоритмов поиска лишь немного усложнив их за счет дополнительного if constexpr.

Комментариев нет: