[英]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.