【问题标题】:Write a function that may return either one or more values编写一个可以返回一个或多个值的函数
【发布时间】:2017-11-08 15:02:21
【问题描述】:

假设我想编写一个函数,例如,返回某个范围内 x 的 f(x) 之和。

double func() {
    double sum = 0.;
    for (int i=0; i<100; i++) {
        sum += f(i);
    }
    return sum;
}

但有时,除了最终的总和,我还需要部分项,所以我可以这样做

pair<vector<double>,double> func_terms() {
    double sum = 0.;
    vector<double> terms(100);
    for (int i=0; i<100; i++) {
        terms[i] = f(i);
        sum += terms[i];
    }
    return {terms, sum};
}

问题是,这是代码重复。在这个例子中这似乎很无害,但是假设函数要大得多(正是在这种情况下促使我提出这个问题),并且两个版本只有几行不同(在这个例子中,逻辑是只有后一个版本在添加到 sum 之前将项存储在向量中,并返回与该向量的对;任何其他逻辑都是等效的)。然后我将不得不编写和维护同一个函数的两个几乎相同的版本,不同之处仅在于几行和 return 语句。我的问题是是否有处理此类问题的习语/模式/最佳实践。可以让我在两个版本之间共享通用代码的东西。

简而言之:我可以编写两个函数并且必须维护两个几乎相同的版本。或者我可以只使用后者,但只要我只需要总和,那将是非常浪费的,这是不可接受的。处理这个问题的最佳模式是什么?


我认为使用 C++17 可以做类似的事情

template<bool partials>
double func(vector<double>* terms=nullptr) {
    double sum = 0.;
    if constexpr (partials)
        *terms = vector<double>(100);
    for (int i=0; i<100; i++) {
        if constexpr (partials) {
            (*terms)[i] = f(i);
            sum += (*terms)[i];
        } else {
            sum += f(i);
        }
    }
    return sum;
}

这与我的意图非常接近,除了使用指针(我不能使用引用,因为terms 可能为空)。

【问题讨论】:

  • 你可以使用optional,它在 boost 和 afaik 中可用,最新的 c++ 版本之一也有它
  • 去掉sum,然后去掉return {terms, std::accumulate(terms.begin(), terms.end(), 0.))} ?
  • @Jarod42,它仍然需要两个函数:一个返回 {terms, std::accumulate...} 另一个返回 {std::accumulate...} 并且没有地址当代码比 std:: 累积调用更复杂时,代码重复的问题似乎是 OP 所建议的
  • 您能否详细说明您在实现double func() { return func_terms().second; } 时在实践中观察到的性能差异?优化器实际上可能比您预期的要好。

标签: c++ idioms


【解决方案1】:

您的问题标题是“编写一个可以返回一个或多个值的函数”,但不止于此;如您的示例所示,该函数还可能在返回结果很久之前做很多不同的事情。对于如此广泛的问题,确实没有通用的解决方案。

但是,对于您所解释的具体情况,我想提供一个技术含量低的解决方案。您可以简单地用第三个函数来实现这两个函数,并给第三个函数一个参数来确定是否执行额外的功能。

这是一个 C++17 示例,其中第三个函数称为 func_impl,或多或少隐藏在命名空间中,以使 funcfunc_terms 的客户端的生活更轻松:

namespace detail {
    enum class FuncOption {
        WithTerms,
        WithoutTerms
    };

    std::tuple<std::vector<double>, double> func_impl(FuncOption option) {
        auto const withTerms = option == FuncOption::WithTerms;
        double sum = 0.;
        std::vector<double> terms(withTerms ? 100 : 0);
        for (int i = 0; i < 100; ++i) {
            auto const result = f(i);
            if (withTerms) {
              terms[i] = result;
            }
            sum += result;
        }
        return std::make_tuple(terms, sum);
    }   
}

double func() {
    using namespace detail;
    return std::get<double>(func_impl(FuncOption::WithTerms));
}

std::tuple<std::vector<double>, double> func_terms() {
    using namespace detail;
    return func_impl(FuncOption::WithoutTerms);
}

这是否低技术取决于你,取决于你的具体问题。

【讨论】:

  • 我的想法几乎是这样的,只是编译器实际上会生成函数的两个版本,并根据option 的值静态分派到适当的函数。因为这里仍然在每次迭代中创建和返回一个向量以及一个分支(尽管编译器可以在循环外托管它,因为withTermsconst),这意味着仍然有充分的理由编写两个单独的版本。
【解决方案2】:

这是一个解决方案,建议将一个可选指针传递给向量并仅在存在时填充它。我删除了它,因为其他答案也提到了它,并且后​​一种解决方案看起来更加优雅。

您可以将您的计算抽象为迭代器,因此调用者仍然非常简单,并且不会复制任何代码:

auto make_transform_counting_iterator(int i) {
    return boost::make_transform_iterator(
            boost::make_counting_iterator(i),
            f);
}

auto my_begin() {
    return make_transform_counting_iterator(0);
}

auto my_end() {
    return make_transform_counting_iterator(100);
}

double only_sum() {
    return std::accumulate(my_begin(), my_end(), 0.0);
}

std::vector<double> fill_terms() {
    std::vector<double> result;
    std::copy(my_begin(), my_end(), std::back_inserter(result));
    return result;
}

【讨论】:

  • @Jarod42 不错的建议,第二个解决方案确实更优雅。我删除了另一个。
【解决方案3】:

一种简单的方法是编写一个通用函数并使用输入参数来做条件。像这样:

double logic(vector<double>* terms) {
    double sum = 0.;
    for (int i=0; i<100; i++) {
        if (terms != NULL) {
            terms.push_back(i);
        }
        sum += terms[i];
    }
    return sum;
}

double func() {
    return logic(NULL);
}


pair<vector<double>,double> func_terms() {
    vector<double> terms;
    double sum = logic(&ret);
    return {terms, sum};
}

此方法在许多情况下使用。逻辑可能非常复杂并且有许多输入选项。您可以通过不同的参数使用相同的逻辑。 但在大多数情况下,我们不需要那么多返回值,只需要不同的输入参数。

【讨论】:

  • 那是非常老式的代码;不仅自从 C++11 引入 nullptr 之后就不再使用 NULL,当我们从 C++17 开始有 std::optional 时使用指针是不好的(以及之前的 boost::optional 或一些自制的可选类那)。当您访问 terms 时,您的代码中也存在编译错误,就好像它是引用而不是指针一样。
【解决方案4】:

如果你不适合:

std::pair<std::vector<double>, double> func_terms() {
    std::vector<double> terms(100);

    for (int i = 0; i != 100; ++i) {
        terms[i] = f(i);
    }
    return {terms, std::accumulate(terms.begin(), terms.end(), 0.)};
}

那么也许:

template <typename Accumulator>
Accumulator& func_helper(Accumulator& acc) {
    for (int i=0; i<100; i++) {
        acc(f(i));
    }
    return acc;
}

double func()
{
    double sum = 0;
    func_helper([&sum](double d) { sum += d; });
    return sum;    
}

std::pair<std::vector<double>, double> func_terms() {
    double sum = 0.;
    std::vector<double> terms;

    func_helper([&](double d) {
        sum += d;
        terms.push_back(d);
    });

    return {terms, sum};
}

【讨论】:

    【解决方案5】:

    对于这种情况,我认为最简单的解决方案是这样的:

    double f(int x) { return x * x; }
    
    auto terms(int count) {
        auto res = vector<double>{};
        generate_n(back_inserter(res), count, [i=0]() mutable {return f(i++);});
        return res;
    }
    
    auto func_terms(int count) {
        const auto ts = terms(count);
        return make_pair(ts, accumulate(begin(ts), end(ts), 0.0));
    }
    
    auto func(int count) {
        return func_terms(count).second;
    }
    

    Live version.

    但是这种方法为func() 提供了与您的原始版本不同的性能特征。当前的 STL 有一些方法可以解决这个问题,但这突出了 STL 不适合可组合性的一个领域。 Ranges v3 库提供了一种更好的方法来组合此类问题的算法,并且正在为未来的 C++ 版本进行标准化。

    通常在可组合性/重用性和最佳性能之间进行权衡。在最好的情况下,C++ 让我们有蛋糕也可以吃,但这是一个例子,正在努力为标准 C++ 提供更好的方法来处理这种情况。

    【讨论】:

      【解决方案6】:

      我制定了一个 OOP 解决方案,其中基类始终计算总和并将当前项提供给派生类,这样:

      class Func
      {
      public:
          Func() { sum = 0.; }
          void func()
          {
              for (int i=0; i<100; i++)
              {
                  double term = f(i);
                  sum += term;
                  useCurrentTerm(term);
              }
          }
          double getSum() const { return sum; }
      
      protected:
          virtual void useCurrentTerm(double) {} //do nothing
      
      private:
          double f(double d){ return d * 42;}
          double sum;
      };
      

      所以派生类可以实现虚方法并包含额外的属性(除了 sum):

      class FuncWithTerms : public Func
      {
      public:
        FuncWithTerms() { terms.reserve(100); }
        std::vector<double> getTerms() const { return terms; }
      
      protected:
        void useCurrentTerm(double t) { terms.push_back(t); }
      
      private:
        std::vector<double> terms;
      };
      

      如果不想公开这些类,可以退回到函数并将它们用作外观(还有 两个 函数,但现在非常易于管理):

      double sum_only_func()
      {
        Func f;
        f.func();
        return f.getSum();
      }
      
      std::pair<std::vector<double>, double> with_terms_func()
      {
        FuncWithTerms fwt;
        fwt.func();
        return { fwt.getTerms(), fwt.getSum() };
      }
      

      【讨论】:

      • 这个想法还不错,解决方案看起来比我自己的要干净一些,但是关于代码的一些细节很奇怪:构造函数中没有初始化列表,getSum 没有const,多余的protected private 就足够了,没有std:: 可能在头文件中的代码中。
      • 我认为您对 const 和 std 的看法是正确的,谢谢,我编辑了代码。
      猜你喜欢
      • 2020-12-16
      • 1970-01-01
      • 2011-02-04
      • 1970-01-01
      • 2022-11-22
      • 2014-09-09
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多