Are there any existing std::variant proposals? If not, we need to get cracking on one.
Axel Naumann is going to write one. I intend to help him with it. The realistic target is C++17,the door on C++14 is closed.
Perhaps it is too early, but I can't help bringing this up already. It looks to me that std::variant will require some compiler magic support in order to provide the Never-Empty gurantee.
Boost.Variant provides the Never-Empty Guarantee without heap allocation, provided that one of your types has a nothrow default constructor. In which case, in the event of a copy failure, the variant will be assigned one of those values.
I don't see the need to impose direct compiler support, just to provide a no-heap guarantee for variants that can't otherwise get one.
On 25 April 2013 01:18, Andrzej Krzemieński <akrz...@gmail.com> wrote:Perhaps it is too early, but I can't help bringing this up already. It looks to me that std::variant will require some compiler magic support in order to provide the Never-Empty gurantee.
What magic support would that be? I'm not seeing it (but it's still early in the morning :-)).
But since it did work on some compilers, this makes me believe that it is implementable at least on certain compilers (UB in the standard may be a well defined behaviour on a particular platform). Every vendor could exploit their own tricks inside the implementation.
On Thu, Apr 25, 2013 at 12:25 PM, Andrzej Krzemieński <akrz...@gmail.com> wrote:
But since it did work on some compilers, this makes me believe that it is implementable at least on certain compilers (UB in the standard may be a well defined behaviour on a particular platform). Every vendor could exploit their own tricks inside the implementation.
I'm almost positive that I seem to recall that, when this was being added to Boost, we regarded the trick as unspecified instead of UB
At the very least, I can't off the top of my head think of a reason for this trick (copy to a different storage then restore it) be anything but unspecified.
And even if it is currently UB, it might be reasonable to change that, similarly to what we did for aligned_storage (which before being added to the std, all actual implementations exploited UB)
Surely, you could implement the copy assignment operator in terms of copy and swap, like normal.
Since swap can now be guaranteed noexcept with move semantics,
it seems to me like this should be a fine implementation strategy for strongly exception safe no double storage no heap allocation copy assignment.
Frankly, I think that requiring nothrow move would be a superior option. It considerably simplifies the implementation, improves performance, and does not have the requirement of needing a nothrow default constructible type to put in the variant in case of failure.
In my opinion, losing the strong exception safety guarantee is something we should only do if we have absolutely no other choice. In this case, we do have a choice.
On Thursday, April 25, 2013 4:36:36 PM UTC+1, Fernando Cacciola wrote:On Thu, Apr 25, 2013 at 12:25 PM, Andrzej Krzemieński <akrz...@gmail.com> wrote:
But since it did work on some compilers, this makes me believe that it is implementable at least on certain compilers (UB in the standard may be a well defined behaviour on a particular platform). Every vendor could exploit their own tricks inside the implementation.
I'm almost positive that I seem to recall that, when this was being added to Boost, we regarded the trick as unspecified instead of UB
[basic.types]/2 says it works for trivially copyable types, which implies it doesn't otherwise, or it wouldn't need saying.
At the very least, I can't off the top of my head think of a reason for this trick (copy to a different storage then restore it) be anything but unspecified.
If an object has a non-trivial destructor you cannot reuse its storage without running the destructor.
What if the object to be replaced had started a new thread which sets a timer and calls a member function on the object if it hasn't been destroyed yet. Since you haven't run its destructor (just moved its bytes to new storage) the other thread will do the callback, but might find garbage where it expects to find the object.
And even if it is currently UB, it might be reasonable to change that, similarly to what we did for aligned_storage (which before being added to the std, all actual implementations exploited UB)
Why was it UB?
The implementation can distinguish a couple cases then:
a) At least one member of the variant has a noexcept(true) default constructor.
b) At most one member of the variant has a noexcept(false) move constructor.
c) The variant is either twice the size or allocates on some assignments.
I don't have a good feeling for whether
std::variant should forbid (c) entirely, or allow it with
allocation-or-double-memory.
To forbid (c), we'd probably have to give the standard containers
noexcept move constructors.
I'm not saying it should be enforced. I'm saying that *if* I install an allocator (not a new-handler since that cannot influence exception guarantees) which behaves this way, then the Standard should specify the noexcepts to take advantage of this.
On 25 April 2013 15:46, Jeffrey Yasskin <jyas...@google.com> wrote:The implementation can distinguish a couple cases then:
a) At least one member of the variant has a noexcept(true) default constructor.
b) At most one member of the variant has a noexcept(false) move constructor.
c) The variant is either twice the size or allocates on some assignments.You can also do some optimizations if you have a nonexcept copy constructor when you are doing copy assignment.
Before trying to solve the never-empty guarantee problem, I think we should examine the design of the variant class.
The design of boost::variant has the following particularity: "By default, a variant default-constructs its first bounded type"
I never felt comfortable with that part of the design of boost::variant. It always felt odd to me that the order of the bounded types matter. In my opinion, a default constructed variant should not have one of its types selected by default. In my personal implementation of a variant class, there is what I call a "null" or "disengaged" or "no type" state. If we think of a variant as being an index representing the selected type along with an aligned storage large enough to hold any of the types of the variant, there would be no extra cost of adding a null state. It could simply be represented by a special index (let's say -1).
template <class... Types>
class variant
{
int selected_type; // value is an index among Types... or -1 if no selected type
std::aligned_storage<MaxLen, MaxAlign> data;
...
There could be a nullvar value that would represent a variant with no type selected (similar to nullopt that is part of the std::optional proposal). For this and a few other things, the variant class would benefit from having a design inspired in part by std::optional.
Why am I bringing this? Because if there is a null state, then the "never empty" problem becomes much simpler. In fact, the problem could then be called "never empty when not nullvar". The copy assignment of distinct types could simply be:
- Destroy old object
- Set selected type as nullvar (could be omitted if copy constructor is noexcept)
- Copy construct new object
- Set selected type as new type
If the copy constructor throws, the variant remains in a nullvar state, which is a valid state.
So, do you think of any problem of having this nullvar state? Maybe there was a reason for not including one in boost::variant but I can't see one apart from being a design choice.
Before trying to solve the never-empty guarantee problem, I think we should examine the design of the variant class.
The design of boost::variant has the following particularity: "By default, a variant default-constructs its first bounded type"
I never felt comfortable with that part of the design of boost::variant. It always felt odd to me that the order of the bounded types matter. In my opinion, a default constructed variant should not have one of its types selected by default. In my personal implementation of a variant class, there is what I call a "null" or "disengaged" or "no type" state. If we think of a variant as being an index representing the selected type along with an aligned storage large enough to hold any of the types of the variant, there would be no extra cost of adding a null state. It could simply be represented by a special index (let's say -1).
template <class... Types>
class variant
{
int selected_type; // value is an index among Types... or -1 if no selected type
std::aligned_storage<MaxLen, MaxAlign> data;
...
There could be a nullvar value that would represent a variant with no type selected (similar to nullopt that is part of the std::optional proposal). For this and a few other things, the variant class would benefit from having a design inspired in part by std::optional.
Why am I bringing this? Because if there is a null state, then the "never empty" problem becomes much simpler.
In fact, the problem could then be called "never empty when not nullvar". The copy assignment of distinct types could simply be:
- Destroy old object
- Set selected type as nullvar (could be omitted if copy constructor is noexcept)
- Copy construct new object
- Set selected type as new type
If the copy constructor throws, the variant remains in a nullvar state, which is a valid state.
So, do you think of any problem of having this nullvar state? Maybe there was a reason for not including one in boost::variant but I can't see one apart from being a design choice.
No it doesn't. OK, it does, but only because you're using wordplay to redefine the problem away.
In the Boost design, a user who doesn't want the "never empty" guarantee doesn't have to pay for it (the presence of `blank` prevents the more expensive copy operation). And a user who does want the "never empty" guarantee, and doesn't want to deal with a "nullvar" state in all of their visitors, will simply not use `blank`.
Everyone wins with the Boost design.
So why should we use an implicit "empty" state when those who want to have an empty state can use an explicit one?
On 29 April 2013 23:42, Nicol Bolas <jmck...@gmail.com> wrote:No it doesn't. OK, it does, but only because you're using wordplay to redefine the problem away.Yes, Alex basically describes at-most-one semantics.In the Boost design, a user who doesn't want the "never empty" guarantee doesn't have to pay for it (the presence of `blank` prevents the more expensive copy operation). And a user who does want the "never empty" guarantee, and doesn't want to deal with a "nullvar" state in all of their visitors, will simply not use `blank`.
Everyone wins with the Boost design.*Experts* win in the Boost design. It is not obvious to non-experts that variant<vector<char>, string> will have some copy/move operations that are significantly slower than expected (given that the *only* constructor required to be marked noexcept is the string move constructor). And, unlike Boost.Variant, we cannot specialize the std type_traits, so there is no choice but to use std::nullopt (or equivalent), which is intrusive in that the user's visitor *must* take it into account. Even if we invented our own type traits just for variant, you still could only specialize them for non-Standard user defined types, which doesn't apply in the case above. In addition, the Boost solution makes the tradeoff of speed over keeping things in the heap (ie, makes yet another copy), so you still don't get full control over what happens. And no one has yet addressed the allocator issue, which I'm pretty sure we'll have to if it can allocate memory and we want the proposal to succeed.Seriously, the number one question I've been asked about variant at my last two jobs is am I sure that a particular instantiation of variant avoids this performance penalty.
So why should we use an implicit "empty" state when those who want to have an empty state can use an explicit one?To not penalize non-experts for using it. Like I said, in order to not dictate implementation, I can't see the exception safety guarantee being any more than "on an exception being thrown, the object is left as an unspecified instance of one of the types", and in practice, I don't find such an unknown state as being useful, but I'm certainly willing to be persuaded by others that it is.
You still need to define what it means to use a visitor on a "nullvar" variant.
Does it throw an exception? Does it simply fail to call any visitation function? Does the user get to have a particular operator() overload that is called on the "nullvar" state? If the latter is true, does every visitor class now have to have this overload?
Pay only for what you use.
Does it throw an exception? Does it simply fail to call any visitation function? Does the user get to have a particular operator() overload that is called on the "nullvar" state? If the latter is true, does every visitor class now have to have this overload?How about throwing if the variant is in a nullvar state and the visitor doesn't provide a nullvar overload? That way, if you are sure that your variant is not nullvar, don't provide a nullvar overload. Otherwise, 2 choices: provide an overload or surround your call to apply visitor by a try-catch.As I understand it, the problem that you are raising is that you don't want to penalize the user by forcing him to implement an overload taking a nullvar_t in every of his visitors. As I said, I would not force him to do so (but it would be an option).
I would push it even further. I would not force visitors to provide overloads for all of the types. If visitor::operator() cannot be called for the current type of the variant, then just throw. Let's say that your variant contains 10 types. You *know* that in a specific context, the variant type should be one of only 3 of these types. So why not allow the implementer of the visitor to only provide overloads for the 3 types that he expect? Just throw if the provided overloads cannot accept the current type of the variant (including nullvar_t).
Pay only for what you use.I couldn't agree more.Notice that I am not going into the details of how it should be implemented but rather on the intent and high level design of the class that will influence how the user will use it. For instance, I am not sure yet how the class will be able to determine if the visitor is providing a specific overload (maybe it will require some compiler support or maybe that sentence should be rephrased -- what I care for now is the intent).
Final note about the "expert"/"non-expert" argument.I can't seem to see how it would benefit "experts" to have the variant copy constructed to the original type (from/to alternate space) if a copy/move assignment fails. If the assignment fails, I might not need to use the variant anymore. So that copy that was made to/from alternate space would just be a waste.
I personally think that an expert will want control over this; copying back to the original type seems an arbitrary choice to me. But we don't have to agree on this if you agree on what I first suggest...
There's a good reason not to do that. One of the nice things about variants are that if you add new elements, you get a compiler error in every piece of code where you forgot handle the new element. Not a runtime error that you may or may not ever hit, a compile-time error that you can't ignore.
Furthermore, if a user knows that it's only in one of three states, it's very easy for them to handle the rest with a: `template<typename T> operator()(const T&) {}` overload. They could even `throw` if they want to. It also happens to make it clear that it's not a mistake for it to not handle the other alternatives; you're explicitlysaying that you're doing nothing for them.
template <class... Types>
class variant
{
// [...]
public:
template <class V, class... VTypes>
struct visitor
{
// Compile error if VTypes are not part of Types
// [...]
};
template <class V>
using complete_visitor = visitor<V, Types...>; // There might be a better name for this
template <class V, class... VTypes>
void apply_visitor(const visitor<V, VTypes...>& v)
{
// Throw if the current type of the variant is not among VTypes
};
// [...]
};
using MyVariantType = variant<int, float, string, vector<int>, list<float>>;
struct MyCompleteVisitor : MyVariantType::complete_visitor
{
void operator()(int& i) const {}
void operator()(float& f) const {}
void operator()(string& s) const {}
void operator()(vector<int>& v) const {}
void operator()(list<float>& l) const {}
// If one of the types in MyVariantType changes or a new one is added,
// this visitor won't compile unless it is modified accordingly
};
struct MyVisitor : MyVariantType::visitor<float, string>
{
void operator()(float& f) const {} // Would not compile without this overload
void operator()(string& s) const {} // Would not compile without this overload
// Other overloads are not specified, so if the variant is of type int,
// vector<int> or list<float>, this visitor would throw when being applied
};
MyVariantType v{0}; // variant constructed as int
v.apply_visitor(MyCompleteVisitor{}); // does not throw
v.apply_visitor(MyVisitor{}); // throws
v = 0.0f;
v.apply_visitor(MyVisitor{}); // does not throw template <class V>
using complete_visitor_with_nullvar = visitor<V, nullvar_t, Types...>; // For sure we can find a better name for this...But you can't separate implementation from intent. It's going to have to be implemented at some point, in real systems. And, unless you intend to stick `variant` in Chapter 17, it's not reasonable to define the requirements such that an implementation would have to rely on compiler-specific intrinsics. Most standard library stuff does not, and that's a good thing. People ought to be able to drop in their own version of the standard library and use it effectively, outside of a select few objects which are intrinsic to C++.
A specification which cannot be implemented reasonably isn't terribly useful. That's why the committee likes to standardize existing practice, or at least provenpractice from a proof-of-concept implementation.
Boost.Variant is already existing practice. If you're going to suggest significant changes, you should prove those changes with an implementation that works and is better in some respect.
Then again, they might need it.
We don't throw away exception safety just because someone might not use the object.
But what you suggest breaks the never-empty guarantee.
template<typename Ret>struct no_error_visitor : public boost:static_visitor<Ret>
{
template<typename T> Ret operator() (const T &) {return Ret();}
};
template<> struct no_error_visitor<void> : public boost::static_visitor<>
{
template<typename T> void operator() {const T&) {}
};
Now how could we integrate nullvar in those visitors? That is another question, but I'm sure it could be done. For example, just accept nullvar_t as one of the types of variant::visitor to force the visitor to define an overload taking a nullvar. Then we could have this defined in the variant class:template <class V>
using complete_visitor_with_nullvar = visitor<V, nullvar_t, Types...>; // For sure we can find a better name for this...But you can't separate implementation from intent. It's going to have to be implemented at some point, in real systems. And, unless you intend to stick `variant` in Chapter 17, it's not reasonable to define the requirements such that an implementation would have to rely on compiler-specific intrinsics. Most standard library stuff does not, and that's a good thing. People ought to be able to drop in their own version of the standard library and use it effectively, outside of a select few objects which are intrinsic to C++.
A specification which cannot be implemented reasonably isn't terribly useful. That's why the committee likes to standardize existing practice, or at least provenpractice from a proof-of-concept implementation.
Boost.Variant is already existing practice. If you're going to suggest significant changes, you should prove those changes with an implementation that works and is better in some respect.Do you really expect everyone here to bring an implementation to be able to discuss?
Please remember that we are in a discussion forum. When I (and others) throw out some ideas like this, I don't have a full proposal and all the possible proofs in my hands right now and I think that is what should be expected. If you only care about fully elaborated proposals and papers, then you should skip this kind of thread. (but it would be sad because you are bringing some valid concerns and clarifications about the ideas being thrown)
But what you suggest breaks the never-empty guarantee.Indeed, that's what I wish: to break that guarantee. For many reasons. There might be some good reasons for this guarantee, but you cannot say that it doesn't have its drawbacks. Personally (and many others), I find it to be more trouble than anything so that's why we shouldn't take it for granted.
Not really. The common case for a variant is to check all of the possibilities. That is, the common case is that it is user error to miss checking one of the elements. The way you're proposing means that I have to expend a lot of effort in order to handle that common case.
Plus, it inextricably links the visitor to the variant that uses it. Is that dependency necessary? Why does the visitation code need to have the variant definition? It also means that it's much more difficult to use a visitor with different variant types. Is there a reason that this should be so difficult to handle?
Also, there is a simpler way than redefining the template operator(), if you're really lazy:template<typename Ret>struct no_error_visitor : public boost:static_visitor<Ret>{template<typename T> Ret operator() (const T &) {return Ret();}};template<> struct no_error_visitor<void> : public boost::static_visitor<>{template<typename T> void operator() {const T&) {}};
Derive all of your visitors from that. See? Problem solved. You can even make a throwing one.
The default case should push errors as close to compile-time as possible. If the user wants to say that they want runtime errors, or just to transparently handle everything else, they have the tools to do that easily. But by default, we should require the user to write proper, fully-qualified visitors.
That's great. The wonderful thing about the Boost version is that you can avoid all of those drawbacks easily enough, just by putting an explicit "empty" state into the variant. See? The copying won't allocate memory; if a copy constructor throws, it will go to the blank state. Boost.Variant guarantees this.
So explain why everyone should be forced to handle empty variants? Boost.Variant gives you everything you ask for (except for the fact that you have to actually handle the empty case. But I consider that a good thing over throwing exceptions). So... what's the problem?
I could add boost::blank as the first of my variant types. The thing is that it is not possible to modify the variant declaration. Let's say there is a function in a library taking a ref to a variant with specific types as a parameter (but these types do not include boost::blank). Now I want to implement that library function and inside of it I do a copy assignment from another variant and I want to avoid the guarantee because I want it to be optimal. It would not make sense to have to modify the library interface (adding boost::blank as one of the types of the variant passed as a ref parameter) just to accommodate the implementer.
Not really. The common case for a variant is to check all of the possibilities. That is, the common case is that it is user error to miss checking one of the elements. The way you're proposing means that I have to expend a lot of effort in order to handle that common case.Yes. If you use variant::complete_visitor, you have the same error-checking as boost::static_visitor. So if that's what you want, I don't have any problem with it being your default.
Plus, it inextricably links the visitor to the variant that uses it. Is that dependency necessary? Why does the visitation code need to have the variant definition? It also means that it's much more difficult to use a visitor with different variant types. Is there a reason that this should be so difficult to handle?I see what you mean. There might be a different way to do it that would avoid it. I think it will work if I make the visitor class not a nested class of variant (put it in namespace scope) and perform the check to verify that the visitor types are all part of the variant types on the call to apply_visitor (at compile-time).
Also, there is a simpler way than redefining the template operator(), if you're really lazy:template<typename Ret>struct no_error_visitor : public boost:static_visitor<Ret>{template<typename T> Ret operator() (const T &) {return Ret();}};template<> struct no_error_visitor<void> : public boost::static_visitor<>{template<typename T> void operator() {const T&) {}};
Derive all of your visitors from that. See? Problem solved. You can even make a throwing one.The problem with doing this is that it won't perform any compile-time checks; visitors will always compile because of the templated overload. With the visitor class I'm proposing, you explicitely define what types should be implemented by the visitor so that the compile-check could be done. If I have a visitor<A, B> my code will not compile if I don't provide overloads taking types A and B.
That's great. The wonderful thing about the Boost version is that you can avoid all of those drawbacks easily enough, just by putting an explicit "empty" state into the variant. See? The copying won't allocate memory; if a copy constructor throws, it will go to the blank state. Boost.Variant guarantees this.
So explain why everyone should be forced to handle empty variants? Boost.Variant gives you everything you ask for (except for the fact that you have to actually handle the empty case. But I consider that a good thing over throwing exceptions). So... what's the problem?The problem, I explained it in my previous post: most of the time it is not possible to add an explicit empty state. Let me cite what I wrote:I could add boost::blank as the first of my variant types. The thing is that it is not possible to modify the variant declaration. Let's say there is a function in a library taking a ref to a variant with specific types as a parameter (but these types do not include boost::blank).
Now I want to implement that library function and inside of it I do a copy assignment from another variant and I want to avoid the guarantee because I want it to be optimal. It would not make sense to have to modify the library interface (adding boost::blank as one of the types of the variant passed as a ref parameter) just to accommodate the implementer.To make it shorter, it doesn't make sens to me to change the types of my variant (which could be used all over my code) just to be able, at a specific place in code, to avoid the cost of the guarantee (if I don't need it in that place in code).