簡體   English   中英

如何將 C++ 中的計算推遲到需要時?

[英]How to defer computation in C++ until needed?

在 C++(*) 中,是否有可能有一個結構“推遲”一些計算直到需要(如果不需要,可能永遠不會進行計算)? 我的用例如下:我有大約十幾個 bool 變量,每個變量都是通過一些函數調用計算出來的。 接下來是一個相當長(且復雜)的條件語句,它以不同的組合使用這些 bool 變量來確定代碼接下來將采取的操作。

這是一些人為的示例代碼,希望能更好地說明我在做什么:

bool const b1 = func1(param1,param2,param3);
bool const b2 = func2(param4);
// ...
bool const b15 = func15(param35,param36,param37,param38);

if (b1 && !b5 && (b2 || b3)) { do_something1(); }
else if (b3 && !b15 || (b4 && b9 && b6)) { do_something2(); }
else if (b14 || b10 || (!b11 && b7)) { do_something3(); }
else if (b8) {
    if (!b1 || !b6) { do_something4(); }
    else if ( /* ... */ ) // ... etc
}
// ... and on and on

這純粹是一個人為的例子,但希望它說明了這個想法。

很明顯,這段代碼可以在沒有 bool 的情況下重寫,並且直接在大條件語句中調用函數。 但我覺得這會使已經不易閱讀的代碼更難閱讀,並且更容易出錯。 而且這個邏輯可能會改變,所以我覺得從重構的角度來看 bool 也更容易管理。

此外,任何 bool 都可能在條件中被多次引用; 所以直接使用這些函數意味着可以重復執行。 (我認為 std::bind 可能會從可讀性的角度讓我到達那里;但它仍然可能多次調用任何 funcN() 調用。)

我正在尋找的是兩個詞中最好的,比如“延遲”計算。 如果不是在代碼開始時明確計算和分配,我可以說,“僅根據需要評估這些(並記住結果)”。 大條件語句是這樣的,一般來說,並不是所有的 bool 實際上都需要計算來確定接下來會發生什么。 這里的目標是提高性能,因為這段代碼經常被調用。 所以我試圖減少每次迭代的工作量。

(*) 最好是 C++14(或更早版本),因為我的雇主正在使用它。

編輯:這樣的事情怎么樣:

#include <iostream>
#include <functional>

//////////////////////////////////////////////////////////////////////////////
class Sum
{
    public:
        int sum(int const a, int const b) { ++n_calls_; return (a+b); }
        int getNCalls() const { return n_calls_; }

    private:
        int n_calls_ = 0;
};

//////////////////////////////////////////////////////////////////////////////
template <class BoundFunc, typename RetType>
class DeferredCompute
{
    public:
        DeferredCompute(BoundFunc const& f) : func_(f) { }

        RetType operator()()
        {
            if (!computed_)
            {
                value_ = func_();
                computed_ = true;
            }
            return value_;
        }

    private:
        bool             computed_ = false;
        RetType          value_;
        BoundFunc const& func_;
};

//////////////////////////////////////////////////////////////////////////////
int main(int argc, char* argv[])
{
    Sum s;
    auto boundSum = std::bind(&Sum::sum, &s, 75, 25);

    DeferredCompute<decltype(boundSum), int> deferredSum(boundSum);

    // call function directly repeatedly
    for (int i=0; i<5; ++i)
    {
      std::cout << "boundSum()=" << boundSum() << std::endl;
    }
    std::cout << "s.getNCalls()=" << s.getNCalls() << std::endl;

    // should only call once
    for (int i=0; i<5; ++i)
    {
      std::cout << "deferredSum()=" << deferredSum() << std::endl;
    }
    std::cout << "s.getNCalls()=" << s.getNCalls() << std::endl;

    return 0;
}

輸出:

boundSum()=100
boundSum()=100
boundSum()=100
boundSum()=100
boundSum()=100
s.getNCalls()=5
deferredSum()=100
deferredSum()=100
deferredSum()=100
deferredSum()=100
deferredSum()=100
s.getNCalls()=6

帶有選項 std::launch::deferred 的 std::async 是您正在尋找的。

https://en.cppreference.com/w/cpp/thread/async

例如

auto future = std::async(std::launch::deferred, [](){return 5;});
// future isn't calculated yet

auto result = future.get();
// result = 5, and will remain cached while in scope.

起初,我會嘗試使用一些 lambda 閉包。

const auto b1 = [&]() { return func1(param1,param2,param3); };
const auto b2 = [&]() { return func2(param4); };
// ...
const auto b15 = [&]() { return func15(param35,param36,param37,param38); };

if (b1() && !b5() && (b2() || b3())) { do_something1(); }
...

如果您需要緩存 bool 結果而不是程序的整個生命周期(靜態),則此解決方案可以實現(三個級別的 lambda 閉包;它是“初始”)。

/**
  g++ -std=c++17 -o prog_cpp prog_cpp.cpp \
      -pedantic -Wall -Wextra -Wconversion -Wno-sign-conversion \
      -g -O0 -UNDEBUG -fsanitize=address,undefined
**/

#include <iostream>

void
test(int i)
{
  auto cache=[](auto expr)
    {
      return [expr, res=false, done=false]() mutable
        {
          if(!done) { res=expr(); done=true; }
          return res;
        };
    };
  auto b1=cache([&]() { std::cout << "(eval b1)"; return i>2; });
  auto b2=cache([&]() { std::cout << "(eval b2)"; return i<5; });
  std::cout << "1: b1=" << b1() << "  b2=" << b2() << '\n';
  std::cout << "2: b1=" << b1() << "  b2=" << b2() << '\n';
}

int
main()
{
  for(int i=0; i<6; ++i)
  {
    std::cout << "~~~~~~~~\n";
    test(i);
  }
  return 0;
}
/**
~~~~~~~~
1: b1=(eval b1)0  b2=(eval b2)1
2: b1=0  b2=1
~~~~~~~~
1: b1=(eval b1)0  b2=(eval b2)1
2: b1=0  b2=1
~~~~~~~~
1: b1=(eval b1)0  b2=(eval b2)1
2: b1=0  b2=1
~~~~~~~~
1: b1=(eval b1)1  b2=(eval b2)1
2: b1=1  b2=1
~~~~~~~~
1: b1=(eval b1)1  b2=(eval b2)1
2: b1=1  b2=1
~~~~~~~~
1: b1=(eval b1)1  b2=(eval b2)0
2: b1=1  b2=0
**/

為了可讀性和可維護性,您可以將程序組織為狀態機。 這為您提供了將狀態轉換和操作彼此分開的好處,而且如果出現必要,稍后重新連接邏輯應該相當簡單。

有關一些示例,請參見此處: 狀態機的 C++ 代碼

如果不是在代碼開始時明確計算和分配,我可以說,“只根據需要評估這些(並記住結果)”

/// @brief only evaluate these as needed (and remember the result)
class lazy final
{
    mutable std::future<bool> value_;
public:
    template<typename Functor>
    lazy(Functor &&f)
    : value_{ std::async(std::launch::deferred,
                         std::forward<Functor>(f)) }
    {
    }

    operator bool() const
    {
         return value_.get();
    }
};

客戶端代碼:

auto b1 = lazy::lazy{[&]{ return func1(param1,param2,param3); }};
auto b2 = lazy::lazy{[&]{ return func2(param4); }};
// ...
bool const b15 = lazy::lazy{[&]{ return func15(param35,param36,param37,param38); }};

// rest remains the same as your contrieved example

我沒有編譯這段代碼。 如果在 c++14 中工作(正如您所提到的),您可能需要一個類似於以下的工廠函數:

template<typename Functor>
auto make_lazy(Functor&& f) { return lazy<Functor>(std::forward<Functor>(f)); }

唯一改變的是你的 bX 變量的聲明。 您還可以考慮添加代碼來告訴您在實踐中調用每個惰性求值的頻率,首先聲明這些 bX 變量,然后立即並行啟動它們,而不是以延遲的方式啟動。 但只有在你衡量兩種方式的性能之后才這樣做。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM