123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580 |
- // an exploration of reference_wrapper in containers. not meant to be read top to bottom, but rather following the function calls, starting from main
- #include <cassert> // observable behavior of out little program here.
- #include <functional> // this is where reference_wrapper lives, probably because it is also very useful in context of function objects, but that's another topic.
- #include <array> // array lives here, we'll use it as an example container.
- // some stuff to do
- #include <algorithm>
- #include <numeric>
- // for going way out of scope with smart pointer and stuff
- #include <memory>
- #include <optional>
- // this is what it should have been called imho,
- // it does not wrap a native reference, it wraps a pointer and models a reference,
- // so i will refer to it as "reference class/object"
- template <typename T>
- struct reference : std::reference_wrapper<T>
- {
- using std::reference_wrapper<T>::reference_wrapper;
- };
- template <typename T> reference(T) -> reference<T>;
- // just a shorthand.
- template <typename T, std::size_t N>
- struct ref_array : std::array<reference<T>, N> {};
- // some magic to infer the array type.
- // everything should work just as well without this,
- // you'd just need to specify the template parameters manually,
- // which we end up doing anyway for some examples
- template <typename F, typename... R> ref_array(F, R...) ->
- ref_array<F, sizeof...(R) + 1>;
- // index
- int main();
- void shared_reference_example();
- void shortcomings();
- void indirect_iteratror_example();
- void maintaining_the_illusion();
- void clever_shared_reference();
- // let's begin our exploration
- int main()
- {
- // reference objects are a basic indirection that can be used in containers to impose a structure on existing set of values/objects, without copying them.
- // (by no means the only or best way to do this, see Boost.Intrusive or Boost.MultiIndex)
- // say we have these objects,
- int a = 1, b = 2, c = 3;
- // and some cool code here that makes use of those names.
- assert( a + b * c == 7 );
- // at some point we would like to perform some repetitive task on all of them, that just begs for a loop,
- // but their names are very distinct and descriptive, and we wouldn't want to replace those with thing[0], thing[1], and thing[2].
- // this is where are array of reference wrappers comes in, allowing us to define an arbitrary order on them and write the loop.
- // (for a more open ended dynamic order you could use a vector, for associations a map, and so on, but we stick to the simplest example here)
- {
- int sum = 0;
- for(auto&& x : ref_array{a,b,c})
- sum += x;
- assert(sum == 6);
- ref_array order{a,b,c};
- assert(accumulate(order.begin(), order.end(), 1, std::multiplies<>{}) == 6);
- }
- // we can store and restructure the container, without affecting the original values
- {
- ref_array scrambled{b,a,c};
- sort(scrambled.begin(), scrambled.end());
- assert((scrambled == ref_array{a,b,c}));
- assert(a == 1 && b == 2 && c == 3);
- }
- // this is similar to how arrays (container objects in general) work in many of popular garbage collected languages with reference semantics (PGCRSL), where it is often a source of many annoying problems with deep copying, immutability and memory management. In contrast to that, here (in addition to having value semantics by default that solve all those problems), when we do introduce this indirection as in these examples, we have a full control of those properties and behaviors of the container.
- // we can have a constant structure with mutable values (oh?!).
- {
- const ref_array csmv{a,b,c};
- // can't rearrange elements
- // swap(csmv.front(), csmv.back()); // ERROR
- // however can mutate the values
- for(auto&& x : csmv)
- x *= 2;
- assert(a == 2 && b == 4 && c == 6);
- }
- // we can have a mutable structure with constant values (unprecedented!).
- {
- ref_array<const int, 3> mscv{a,b,c};
- // now we can rearrange
- swap(mscv.front(), mscv.back());
- assert((mscv == ref_array<const int,3>{c,b,a}));
- // and can't mutate values
- // for(auto&& x : mscv)
- // x *= 2; // ERROR
- assert(a == 2 && b == 4 && c == 6);
- }
- // we can have a constant structure with constant values (impossible!).
- {
- const ref_array<const int, 3> cscv{a,b,c};
- // can't change anything
- // swap(cscv.front(), cscv.back()); // ERROR
- // for(auto&& x : cscv)
- // x *= 2; // ERROR
- assert(a == 2 && b == 4 && c == 6);
- }
- // and, of coarse - mutable structure with mutable values.
- {
- // can change anything we want.
- ref_array msmv{a,b,c};
- swap(msmv.front(), msmv.back());
- assert((msmv == ref_array{c,b,a}));
- for(auto&& x : msmv)
- x /= 2;
- assert(a == 1 && b == 2 && c == 3);
- }
- {
- ref_array abc{a,b,c};
- // to shallow copy we can use a reference structure.
- ref_array shallow {abc};
- for(auto&& x : shallow)
- ++x;
- assert(a == 2 && b == 3 && c == 4);
- // to deep copy - value structure.
- std::array<int,3> deep;
- copy(abc.begin(), abc.end(), deep.begin());
- for(auto&& x : deep)
- x += 1000;
- assert((deep == std::array{1002,1003,1004}));
- assert(a == 2 && b == 3 && c == 4);
- }
- // by default the references are non owning as usual, but in theory we can have an analogous smart reference class, that will wrap a smart pointer instead of a raw pointer.
- shared_reference_example();
- // there are several shortcoming with reference objects though, that we (unintentionally) avoided so far, so lets take a look at some cases where things aren't so easy
- shortcomings();
- return 0;
- }
- // minimal example of shared pointer based reference object
- template <typename T>
- class shared_ref
- {
- public:
- shared_ref(const std::shared_ptr<T>& ptr) : ptr(ptr) {}
- operator T& () { return *ptr; }
- private:
- std::shared_ptr<T> ptr;
- };
- // and the accompanying value simulating type
- template <typename T>
- class shared_val
- {
- public:
- shared_val(const std::shared_ptr<T>& ptr) : ptr(std::make_shared<T>(*ptr)) {}
- shared_val(const shared_val<T>& other) : ptr(std::make_shared<T>(*other.ptr)) {}
- operator T& () { return *ptr; }
- operator shared_ref<T> () { return shared_ref{ptr}; }
- private:
- std::shared_ptr<T> ptr;
- };
- // same shorthand as before but for shared_ref and shared_val
- template <typename T, std::size_t N>
- struct shared_ref_array : std::array<shared_ref<T>, N> {};
- template <typename F, typename... R> shared_ref_array(std::shared_ptr<F>, std::shared_ptr<R>...) ->
- shared_ref_array<F, sizeof...(R) + 1>;
- template <typename T, std::size_t N>
- struct shared_val_array : std::array<shared_val<T>, N> {};
- template <typename F, typename... R> shared_val_array(std::shared_ptr<F>, std::shared_ptr<R>...) ->
- shared_val_array<F, sizeof...(R) + 1>;
- // a bit of interop between shared ref and shared val... could be better
- template <typename F, typename... R> shared_ref_array(shared_val<F>, shared_val<R>...) ->
- shared_ref_array<F, sizeof...(R) + 1>;
- void shared_reference_example()
- {
- // as we know the shared pointer lives in a world of it's own, so our reference will have to live there too
- auto a = std::make_shared<int>(1);
- auto b = std::make_shared<int>(2);
- auto c = std::make_shared<int>(3);
- shared_ref g = a;
- int sum = 0;
- for(auto&& x : shared_ref_array{a,b,c})
- sum += x;
- assert(sum == 6);
- for(auto&& x : shared_ref_array{a,b,c})
- x += 1;
- assert(*a == 2 && *b == 3 && *c == 4);
- // but does this mean that like PGCRSLs we lost value semantics?? well yes and no, we lost it, but only circumstantially, we can bring it back by introducing a shared value(kinda like https://hackernoon.com/value-ptr-the-missing-c-smart-pointer-1f515664153e). this combination now allows us to choose freely between value and reference, not at class definition, like some sad programmers that invented C# do, but at the point of use, like proper dignified programmers. And before you go, "oh nooo not the copy, we can't allow copy, muh speeeed, muh memryyy", consider that, with tools available in the language today, a copy should be a business logic more often than it would be a bottleneck, and the inability(or extremely unwieldy ability) is common problem in PGCRSLs.
- std::optional<shared_ref_array<int,3>> far_away_land; // we also take this opportunity to write a basic example of the shared references at work
- {
- auto deep_copy = shared_val_array{a,b,c};
- for(auto&& x : deep_copy)
- x *= x;
- assert(*a == 2 && *b == 3 && *c == 4);
- assert(deep_copy[0] == 4 && deep_copy[1] == 9 && deep_copy[2] == 16);
- far_away_land = shared_ref_array{deep_copy[0], deep_copy[1], deep_copy[2]}; // can't we just far_away_land = deep_copy? sure, with a bit of work we could, but that's a bit out of scope, more about the array class than anything else
- }
- assert((*far_away_land)[0] == 4 && (*far_away_land)[1] == 9 && (*far_away_land)[2] == 16);
- for(auto&& x : *far_away_land)
- x *= x;
- assert((*far_away_land)[0] == 16 && (*far_away_land)[1] == 81 && (*far_away_land)[2] == 256);
- assert(*a == 2 && *b == 3 && *c == 4);
- // also keep in mind that while this kind of memory management patterns might be tempting, promising to eliminate dangling pointers and memory leaks, in practice they are prone to analogous logical errors, such as null dereference or unnecessary lifetime extension, very common issues in PGCRSL. also note that shared_pointer is not a garbage collector, it can't handle cycles and is not going to minmax the allocations/deallocations for you. though such a smart pointer could exist (https://github.com/crazybie/tgc2).
- }
- // example of constraining a generic function through type deduction/matching
- template <typename T> T quadrance(const std::array<T,2>&);
- // example of constraining a generic function using SFINAE with type traits
- template <typename T,
- std::enable_if_t<std::is_arithmetic_v<T>>* = nullptr>
- T square(const T& x) { return x * x; }
- void shortcomings()
- {
- int a = -1, b = 2, c = -3;
- // let's take a closer look at the very first example,
- {
- int sum = 0;
- for(auto&& x : ref_array{a,b,c})
- sum += x; // <-- this is where the magic happens
- // the += operator is like a function that accepts an int& as first parameter, and the reference object is implicitly converted.
- // conceptually there is no ambiguity, it doesn't make sense to accumulate references so it's only sensible to operate on the referred values.
- // however we can easily try something more ambiguous (again conceptually),
- // for(auto&& x : ref_array{a,b,c})
- // x = abs(x); // ERROR
- // this is interpreted as a re-assignment of reference and not the value, and we cannot refer to a temporary that is returned by abs() function.
- // there is a bit of a conceptual fork here, do we want to change the indirection or the underlying value*?
- for(int& x : ref_array{a,b,c}) // <-- we can get value assignment by forcing the conversion here, but that is rather circumstantial
- x = abs(x);
- assert(a == 1 && b == 2 && c == 3);
- // * one could argue that since we can't refer to a temporary, this specific case should be silently treated as a value assignment (and we will explore such approaches too), however that does not solve the problem of the assignment in general. Others would argue that the loud clear failure is better that silent surprising success, and that reference object is already too clever with the implicit conversion that should instead be explicit.
- }
- // we stumble upon similar issues with member access - do we want a member of the reference object or the value?
- // again the standard reference class doesn't try anything clever here, it assumes we want the top level object unless we spell it out explicitly,
- {
- struct { int l,r; } a{1,2}, b{3,4}, c{5,6};
- assert(b.r - b.l == 1);
- // for(auto&& x : ref_array{b,a,c})
- // assert(x.r - x.l == 1); // ERROR
- for(decltype(a)& x : ref_array{b,a,c}) // <-- explicit conversion
- assert(x.r - x.l == 1);
- }
- // other issues come up in generic programming with type constraints, either through type deduction
- {
- std::array a = {1,2}, b = {1,2}, c = {1,2};
- assert(quadrance(a) == 5);
- // for(auto&& x : ref_array{b,a,c})
- // assert(quadrance(x) == 5); // ERROR
- }
- // or SFINAE and type traits.
- {
- int a = 5, b = 5, c = 5;
- assert(square(a) == 25);
- // for(auto&& x : ref_array{a,b,c})
- // assert(square(x) == 25); // ERROR
- }
- // there are different ways to address these shortcomings, that we can roughly divide in two categories
- // we can contextually drill through the indirection (explicit)
- indirect_iteratror_example();
- // or avoid these kind of forks and do our best to maintain the illusion of transparent reference objects (implicit)
- maintaining_the_illusion();
- }
- // this is not a proper implementation of an iterator, just a proof of concept (see Boost.IteratorAdaptor)
- template <typename It>
- struct value_iterator
- {
- It base;
- value_iterator(It base) : base(base) {}
- value_iterator& operator++() { ++base; return *this; }
- friend bool operator==(const value_iterator& a, const value_iterator& b) { return a.base == b.base; }
- friend bool operator!=(const value_iterator& a, const value_iterator& b) { return !(a == b); }
- const auto& operator*() const { return *(base).get(); }
- auto& operator*() { return base->get(); }
- };
- template <typename T>
- struct value_range
- {
- using base_iterator = typename T::iterator;
- value_iterator<base_iterator> _begin, _end;
- value_range(T& x) :
- _begin([&x](){ using std::begin; return begin(x); }()),
- _end([&x](){ using std::end; return end(x); }())
- {}
- };
- template <typename T>
- auto begin(value_range<T>& x) { return x._begin; }
- template <typename T>
- auto end(value_range<T>& x) { return x._end; }
- void indirect_iteratror_example()
- {
- // since dealing with containers more often than not we use iterators, we can abuse that indirection to solve all our problems.
- // assignment
- {
- int a = -1, b = 2, c = -3;
- ref_array order{a,b,c}; // RANGE SIDENOTE: important to give it a name to keep it alive #range_based_for_loop_problems
- for(auto&& x : value_range(order))
- x = abs(x);
- assert(a == 1 && b == 2 && c == 3);
- }
- {
- int a = -1, b = 2, c = -3;
- ref_array order{a,b,c}; // RANGE SIDENOTE: not a problem or a confusion, but an obvious necessity with the oldern explicit begin-end ways... makes ya thonk
- std::transform(value_iterator(begin(order)), value_iterator(end(order)),
- value_iterator(begin(order)), [](auto&& x){ return std::abs(x);} ); // RANGE SIDENOTE: so verbose, though it's our iterator's and std::abs's fault largely
- assert(a == 1 && b == 2 && c == 3);
- }
- // member access
- {
- struct { int l,r; } a{1,2}, b{3,4}, c{5,6};
- assert(b.r - b.l == 1);
- ref_array order{b,a,c};
- for(auto&& x : value_range(order))
- assert(x.r - x.l == 1);
- }
- // template type matching
- {
- std::array a = {1,2}, b = {1,2}, c = {1,2};
- assert(quadrance(a) == 5);
- ref_array order{b,a,c};
- for(auto&& x : value_range(order))
- assert(quadrance(x) == 5);
- }
- // SFINAE
- {
- int a = 5, b = 5, c = 5;
- assert(square(a) == 25);
- ref_array order{b,a,c};
- for(auto&& x : value_range(order))
- assert(square(x) == 25);
- }
- // in these trivial examples this solution might seem equivalent to the explicit cast/type specification in the for loop, but the verbose version with std::transform should make the difference apparent - the explicit choice of value vs reference happens on the side/level that defines the reference structure, so generic code can be used without any changes. an explicit transparency if you will, unlike the next approach we'll explore that will pose all sorts of requirements on all generic/library code, to achieve implicit transparency on user's side.
- }
- // just a trait to identify our reference objects
- template <typename T> struct is_reference_object : std::false_type {};
- template <typename T> struct is_reference_object<reference<T>> : std::true_type {};
- template <typename T> constexpr auto is_reference_object_v = is_reference_object<T>::value;
- // here is an approach to type constraints supporting reference objects,
- // similar to what you would do when having to deal with native references generically
- // first component is a decay type function that goes through reference objects,
- // analogous to (and making use of) std::decay
- template <typename T, typename Enabled = void>
- struct decay_ref { using type = std::decay_t<T>; };
- template <typename T>
- struct decay_ref<T,
- std::enable_if_t<is_reference_object_v<std::decay_t<T>>> >
- { using type = typename std::decay_t<T>::type; };
- template <typename T>
- using decay_ref_t = typename decay_ref<T>::type;
- // the second component a forwarding function,
- // again analogous to std::forward,
- template <class T, std::enable_if_t<not is_reference_object_v<std::decay_t<T>>>* = nullptr>
- decltype(auto) forward_ref(T& t) noexcept
- {
- return std::forward<T>(t);
- }
- // except unwrapping reference wrappers.
- template <class T, std::enable_if_t<is_reference_object_v<std::decay_t<T>>>* = nullptr>
- decltype(auto) forward_ref(T& t) noexcept
- {
- return t.get();
- }
- // the square function with type constraints supporting reference objects
- // could also forward x, or take it by const reference
- template <typename T,
- std::enable_if_t<std::is_arithmetic_v<decay_ref_t<T>>>* = nullptr>
- decltype(auto) decay_square(T&& x) { return x * x; }
- // we can no longer rely on type deduction for quadrance, so here is a rudimentary SFINAE tool to achieve the same thing
- template <typename T> struct is_array2 : std::false_type {};
- template <typename T> struct is_array2<std::array<T,2>> : std::true_type {};
- // properly type constrained version of quadrance
- template <typename T, std::enable_if_t<is_array2<decay_ref_t<T>>{}>* = nullptr>
- typename decay_ref_t<T>::value_type decay_quadrance(T&& vector);
- // a struct with members, omg
- template <typename T>
- struct ab
- {
- T a, b;
- };
- // here is how we get to them, also could type constrain
- template <typename T> decltype(auto) a_(T&& x)
- { return (forward_ref<T>(x).a); }
- template <typename T> decltype(auto) b_(T&& x)
- { return (forward_ref<T>(x).b); }
- // a bit much, the upside - can use the same getter for different classes in same namespace
- template <typename T>
- class clever_ref
- {
- public:
- clever_ref(T& val) : ptr(&val) {}
- clever_ref(T&& ptr) = delete;
- operator T& () { return *ptr; }
- clever_ref& operator=(T& val) { ptr = val; return *this; }
- clever_ref& operator=(T&& val) { *ptr = val; return *this; }
- private:
- T* ptr;
- };
- // same shorthand for clever_ref
- template <typename T, std::size_t N>
- struct clever_ref_array : std::array<clever_ref<T>, N> {};
- // some magic to infer the array type.
- // everything should work just as well without this,
- // you'd just need to specify the template parameters manually,
- // which we end up doing anyway for some examples
- template <typename F, typename... R> clever_ref_array(F, R...) ->
- clever_ref_array<F, sizeof...(R) + 1>;
- void maintaining_the_illusion()
- {
- // here we are not going to respect any boundaries or existing practices and do everything in our power to make the reference objects disappear like native ones(which never existed, as objects that is).
- // lets start with the meta programming - argument deduction and type constraint.
- // we can't really fix type deduction so we just replace is with SFINAE type constraint and fix SFINAE by an analogy to a technique used for type constraint with perfect forwarding
- // argument deduction example(but not really argument deduction anymore)
- {
- int a = 1, b = 2, c = 3;
- int z = 4;
- reference z_ref = z;
- assert(decay_square(z) == 16);
- assert(decay_square(z_ref) == 16);
- }
- {
- std::array a = {1,2}, b = {1,2}, c = {1,2};
- reference x = a;
- const std::array<int,2>& f = x;
- for(auto&& x : ref_array{b,a,c})
- assert(decay_quadrance(x) == 5);
- }
- // SFINAE
- {
- int a = 5, b = 5, c = 5;
- for(auto&& x : ref_array{b,a,c})
- assert(decay_square(x) == 25);
- for(auto&& x : std::array{b,a,c})
- assert(decay_square(x) == 25);
- }
- // now lets take care of member access by outright disallowing it in interfaces, no members at all on the calling side, forget they existed, all classes are required to provide free functions instead of(or along with) public member functions, and free function getters for all public member variables. The generic way to set up these getters would be with perfect forwarding, so we will again abuse this abstraction to slide in reference object handling.
- {
- ab<int> x{1,2};
- reference ba = x;
- auto& a = a_(x);
- auto& b = b_(x);
- assert(a == 1 && b == 2);
- a = 12;
- b = 13;
- assert(x.a == 12 && x.b == 13);
- assert(a_(ab<int>{3,4}) == 3);
- }
- // note that we are converting the reference object to native reference, changing the assignment behavior. In theory it shouldn't be a problem, since assignment to a forwarded variable does not make much sense (why assign to an rvalue?), but it might come up in some generic code, so we can't apply this kind of forwarding across the board, unlike decay_ref which is more or less universal.
- // while we could just choose free function over members functions, as it's more or less a syntactic feature*, we can't quite make such a choice for assignment**, as losing either reference or value assignment would be a major functional handicap (i'm looking at you PGCRSLs).
- // one sensible approach would be to just revert to indirect iterators for disambiguating assignment, however we're going to dig deeper and see exactly how clever we can get. Lets try out a new clever reference object that will assign temporaries by value, since, as mentioned before, we can't*** refer to them anyway.
- {
- int a = -1, b = 2, c = -3;
- for(auto&& x : clever_ref_array{b,a,c})
- x = abs(x);
- assert(a == 1 && b == 2 && c == 3);
- }
- // with all that said, the costs usually outweighs the benefits with these kind of patterns, so you might want to just forget all this, and deal with reference objects when they come up either explicitly or using the indirect iterators... unless if you are directly translating known working code from some PGCRSL, then maybe that small island can adhere to these traditions, until properly deprecated.
- // * but won't saying no to member functions mean saying no to virtual functions? not really, we're saying no to them in interfaces, but we can still use them as implementation details and wrap them in free functions
- // ** a member function that cannot be free. coincidence? I think not!
- // *** really tho? akshuhlly, with the shared reference that we looked at before we can just allocate new storage in place and reassign the reference. This can be useful when working with immutable values and is yet another feature PGCRSLs conditionally(sneakily) support under the hood.
- clever_shared_reference();
- }
- void clever_shared_reference()
- {
- }
- // implementation details
- template <typename T> T quadrance(const std::array<T,2>& vector)
- {
- T result{};
- for(auto&& x : vector)
- {
- result += x*x;
- }
- return result;
- };
- // for quadrance we also need to "hijack" begin and end function, since they are not aware of our shenanigans
- template <typename T, std::enable_if_t<is_array2<decay_ref_t<T>>{}>* = nullptr>
- auto begin(T&& container)
- {
- return std::begin<decay_ref_t<T>>(container);
- }
- template <typename T, std::enable_if_t<is_array2<decay_ref_t<T>>{}>* = nullptr>
- auto end(T&& container)
- {
- return std::end<decay_ref_t<T>>(container);
- }
- template <typename T, std::enable_if_t<is_array2<decay_ref_t<T>>{}>*>
- typename decay_ref_t<T>::value_type decay_quadrance(T&& vector)
- {
- typename decay_ref_t<T>::value_type result{};
- for(auto&& x : vector)
- {
- result += x*x;
- }
- return result;
- // using std::begin;
- // using std::end;
- // return std::accumulate(begin(vector), end(vector),
- // typename decay_ref_t<T>::value_type{},
- // [](auto& a, auto& x) { return a + x * x; }
- // );
- }
|