Date: Mon, 7 Sep 2020 16:49:51 +0000
Dear all,
[I mistakenly started this discussion in the std-discussion mail list (with the same subject), so now transferring it here.]
[I searched the list's archive but didn't find anything relevant.]
Problem: How to change the value of a variant from within a visitor? Unfortunately, the relevant operator()(Alternative&) of the visitor has no access to the enclosing std::variant. Not only this problem is annoying, but it's also messy to work around, as explained below.
Proposed solution: It would be convenient to have a standard function, tentatively named variant_from_alternative, that would turn Alternative& to variant& (with overloads for const& and &&). It can be implemented with 0 cost via a type cast, provided the storage for alternatives is the first field within the variant (otherwise one has to subtract the offset of the storage, which is not 0-cost but still cheap). This works even if the same type occurs multiple times in the template parameters of std::variant.
Motivating Example 1: Visitor turning a Caterpillar to a Butterfly:
using MyVariant = variant<Caterpillar,Butterfly>;
struct Visitor{
void operator()(Caterpillar& c){ variant_from_alternative<MyVariant>(c) = Butterfly(); }
...
};
Motivating Example 2: A practical scenario would be processing of tree-like structures e.g. terms or parse trees/ASTs; e.g. a visitor for simplifying algebraic expressions may want to replace a subtree of the form 0*ANY by 0, which changes the variant's alternative from Multiply to Number. Visitors for ASTs deal with many alternatives and call itself recursively, so lambdas are inconvenient.
Potential problems:
(1) this function would be more difficult to implement, and would require overheads like back-pointers, if an analogue of boost::reference_wrapper is to be supported in future, but my understanding is that this is unlikely;
(2) if a Caterpillar object is stand-alone and not within a variant, or the variant type is incorrectly specified, the function will return an invalid reference (which may result in UB later on), so it's the responsibility of the programmer to use this function correctly. This risk is mitigated by: (i) passing the whole variant type rather than the list of its template parameters (which may be mistakenly given in a wrong order) to the function - one would normally expect the programmer to typedef variants, as their definitions tend to be long - the natural laziness ensures one does not write a variant definition more than once; (ii) by a static check that the template parameter is std::variant and that the alternative occurs in its type list.
I accept that even with these mitigations there is a risk of UB, but I'd argue that this solution is very convenient, solves a practical problem, and results in cleanest code compared to the workarounds below, so the advantages far outweigh the risks.
Workarounds: The workarounds discussed in the std-discussion mail list were as follows:
* Store a pointer (or reference) to std::variant inside a visitor. This entails defining a constructor and/or some mechanism for updating this pointer if the visitor object is to be re-used (re-using the visitor is common for recursive processing of tree-like structures); this can be mitigated to some degree by inheriting/wrapping the visitor from/in a class that manages the pointer. This workaround is ugly if the variant is an rvalue - one ends up holding references to the variant itself and to the alternative, i.e. breaking unique ownership. If the visitor calls itself recursively (common in AST processing), one must remember always to update the stored pointer - this is error-prone if there are many calling sites (again, common for ASTs).
* Using a lambda as a visitor, capturing the variant. This is conceptually similar to the above (and so subject to the same problems), but cleaner in some cases; on the other hand, it is unsuitable if there are many alternatives each requiring custom processing (common for ASTs). Also, there are problems updating the captured reference if the visitor is to be re-used, e.g. calls itself recursively.
* Create another visitation function, that would pass a reference to the variant to the visitor as an extra parameter. This work-around removes the risk of UB, but it requires all visitor's operator()s to have an extra parameter, even if only a few use it. Moreover, it has a problem if the variant is an rvalue - one ends up with two references to essentially the same object, which breaks unique ownership.
What do you think?
Victor.
[I mistakenly started this discussion in the std-discussion mail list (with the same subject), so now transferring it here.]
[I searched the list's archive but didn't find anything relevant.]
Problem: How to change the value of a variant from within a visitor? Unfortunately, the relevant operator()(Alternative&) of the visitor has no access to the enclosing std::variant. Not only this problem is annoying, but it's also messy to work around, as explained below.
Proposed solution: It would be convenient to have a standard function, tentatively named variant_from_alternative, that would turn Alternative& to variant& (with overloads for const& and &&). It can be implemented with 0 cost via a type cast, provided the storage for alternatives is the first field within the variant (otherwise one has to subtract the offset of the storage, which is not 0-cost but still cheap). This works even if the same type occurs multiple times in the template parameters of std::variant.
Motivating Example 1: Visitor turning a Caterpillar to a Butterfly:
using MyVariant = variant<Caterpillar,Butterfly>;
struct Visitor{
void operator()(Caterpillar& c){ variant_from_alternative<MyVariant>(c) = Butterfly(); }
...
};
Motivating Example 2: A practical scenario would be processing of tree-like structures e.g. terms or parse trees/ASTs; e.g. a visitor for simplifying algebraic expressions may want to replace a subtree of the form 0*ANY by 0, which changes the variant's alternative from Multiply to Number. Visitors for ASTs deal with many alternatives and call itself recursively, so lambdas are inconvenient.
Potential problems:
(1) this function would be more difficult to implement, and would require overheads like back-pointers, if an analogue of boost::reference_wrapper is to be supported in future, but my understanding is that this is unlikely;
(2) if a Caterpillar object is stand-alone and not within a variant, or the variant type is incorrectly specified, the function will return an invalid reference (which may result in UB later on), so it's the responsibility of the programmer to use this function correctly. This risk is mitigated by: (i) passing the whole variant type rather than the list of its template parameters (which may be mistakenly given in a wrong order) to the function - one would normally expect the programmer to typedef variants, as their definitions tend to be long - the natural laziness ensures one does not write a variant definition more than once; (ii) by a static check that the template parameter is std::variant and that the alternative occurs in its type list.
I accept that even with these mitigations there is a risk of UB, but I'd argue that this solution is very convenient, solves a practical problem, and results in cleanest code compared to the workarounds below, so the advantages far outweigh the risks.
Workarounds: The workarounds discussed in the std-discussion mail list were as follows:
* Store a pointer (or reference) to std::variant inside a visitor. This entails defining a constructor and/or some mechanism for updating this pointer if the visitor object is to be re-used (re-using the visitor is common for recursive processing of tree-like structures); this can be mitigated to some degree by inheriting/wrapping the visitor from/in a class that manages the pointer. This workaround is ugly if the variant is an rvalue - one ends up holding references to the variant itself and to the alternative, i.e. breaking unique ownership. If the visitor calls itself recursively (common in AST processing), one must remember always to update the stored pointer - this is error-prone if there are many calling sites (again, common for ASTs).
* Using a lambda as a visitor, capturing the variant. This is conceptually similar to the above (and so subject to the same problems), but cleaner in some cases; on the other hand, it is unsuitable if there are many alternatives each requiring custom processing (common for ASTs). Also, there are problems updating the captured reference if the visitor is to be re-used, e.g. calls itself recursively.
* Create another visitation function, that would pass a reference to the variant to the visitor as an extra parameter. This work-around removes the risk of UB, but it requires all visitor's operator()s to have an extra parameter, even if only a few use it. Moreover, it has a problem if the variant is an rvalue - one ends up with two references to essentially the same object, which breaks unique ownership.
What do you think?
Victor.
Received on 2020-09-07 11:53:24