Lambdas!
Yes. That divides modern C++ man, from old C++ boys! We remember the factory pattern and we will use it here too. Thus showing the unmistakable OO pedigree, too.
We will deliver the function template returning a lambda! Close to real stunt, but works And compiler has fewer problems disambiguating the instances
1 2 3 4 5 6 7 8 9 10 11 12 13 |
/* */ template<typename T> inline auto holder_maker(T defval_) { // no T new_ = defval_ will not compile, try... return [&](const T * new_ = nullptr) { static T default_value = defval_; if (new_ && *new_ != default_value) default_value = *new_; return default_value; }; }; |
Looks much more in line with our “c++ ninja” status. Does it not? Very clever little stunt that is!
Testing
1 2 3 4 5 6 7 8 9 10 11 12 |
namespace { using namespace Gdiplus; auto default_smoot = holder_maker<SmoothingMode>( SmoothingMode::SmoothingModeAntiAlias ); auto default_lncap = holder_maker<LineCap>(LineCap::LineCapRound); auto default_width = holder_maker<Gdiplus::REAL>(10); } |
Also. The real modern C++ thinking shines through. Functional programming and the rest. Function pointers are nicely (and obediently by the compiler) made. auto
is used properly so there are no ugly pointers to instances of template functions. Or was it function templates?
But. Few little problems here too. First this pesky default argument issue. It seems the good old argument as “pointer to null” works the best as the default argument idiom. It seems it is far from modern C++. We try the “modern” solution on that line:
return [&](const T & new_ = defval_) { ... }
But alas no can do:
error C2587: 'defval_': illegal use of local variable as default parameter
Oh? That is a bit of a surprise. We quickly “Google out” for some advice but it seems nothing works. Time passes by. Fine. Is this really a problem?
Well yes, it is. If we pass the pointer we need to guarantee it points into the meaningful value. This line is especially troublesome:
default_value = *new_;
We make two big assumptions here: first that de-referencing will produce a meaningful value of type T, and second that operator “=” exists and can take the right-hand side as we wish it to.
Hmm. Not entirely sure here. Should we try hard and pull some latest modern C++ tricks here? C++17 guides? Or some yet undiscovered nugget from the std lib.
And yes we also have noticed this solution does not work as we expect it to. It simply does not return the default value on the first call? Hm. Hm.
Working option Two
I am not an omnipotent C++ guru but let me jump in now and help our bright but somewhat unfortunate programmer and help provide a C++17 solution. With errors in logic removed too.
As before we will stay with the idea of function template returning a lambda. So what do we do to make this behave as we want it to?
On the first call, we execute the lambda and return it. Thus on the second and all the other calls, it will be used with a proper def value from the first call. You know: if we try simply T new_ = defval_
that will not compile.
The elegant (C++17) way around, is to use std::optional
(NOTE: lambda bellow requires T to be a movable type so if T is your class it better be movable)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
#include <iostream> #include <optional> // https://godbolt.org/z/f9Mafa /* outer is a function called initially it requires default value as argument */ template<typename T> inline auto holder_maker(T defval_) { /* inner lambda is returned */ auto defval_handler = [&](const std::optional<T> & new_ = std::nullopt) noexcept -> const T && { static T default_value = defval_; if (new_.has_value() ) { T new_val_ = new_.value_or(default_value); if (new_val_ != default_value) default_value = new_val_ ; } return std::forward<T>( default_value ); }; // we provoke a first time call // with std::nullopt static auto first_call_ = defval_handler(); // result is a inner lambda // for all subsequent calls return defval_handler; } // quick test int main (void ) { auto int_holder = holder_maker<int>(42) ; std::cout << int_holder() << "\n" ; // use the default int int_holder(13) ; // change it std::cout << int_holder() << "\n" ; // use the default int return 42 ; } |
Yes, this is a real modern C++17. std::optional<>
and a such. And it works too. So what could be possibly wrong with it?
The reality bites. It is very likely:
- Your team/company is still on C++11, or is it C++03?
std::optional
requires (at least) C++17
- To your team, this code is pretty hard to understand
- yes this is clever but definitely on the edge of “stunt”
- To maintain or expand this code authors presence is required
- Here insert your favorite show stopper …
What should we do? We should stop here and think. How would we actually implement this if we focus on the requirements instead of the latest features possible with the programming language we use. After all, if something is in there, in modern C++, that does not mean we have to use it.
Imperative (in case you forgot) is to solve the requirements. In a feasible manner.