
On Mon, Jul 6, 2009 at 6:03 PM, Vladimir Batov<vladimir.batov@wrsa.com.au> wrote:
vicente.botet <vicente.botet <at> wanadoo.fr> writes: I have read the documentation and I think that you are mixing default value with error value.
I am not sure I can agree. In my book there is *no* separate default value. There is only one value -- the conversion-failure (called default, error, etc.) value.
The fact that a type is not DefaultConstructible do not implies that the user wants to have an exception when the conversion fails. This are two orthogonal dimensions.
I think you are saying that "The fact that a type is not DefaultConstructible does not mean that the user *does not* want to have an exception when the conversion fails" because when the default/error value is provided, there is *no* exception on failure.
And indeed, I strongly agree that these two issues -- the throwing behavior and providing/not-providing the default/error value -- are indeed orthogonal. I am very glad you mention that because IMHO it is a subtle but important issue that is often missed or misunderstood.
default and error are *mostly* orthogonal default and DefaultConstructible are *mostly* orthogonal as well: default and error: - if I supply a default, then I do NOT expect an error - if we did (as once suggested) int x = convert<int>::from(str, 0, &error); then I supply a default so that x is set to something, but still want any error (warning?) info default and DefaultConstructible: - if the To type is NOT DefaultConstructible, then if I want a default, I need to supply one - if the To type IS DefaultConstructible, then I might STILL want to pass in a default: int min = convert<int>::from(minStr, 0); // get min or assume 0 int max = convert<int>::from(maxStr, 100); // get max, else 100 so for a given To type: if (To is DefaultConstructible) { if (specific default requested) { int max = convert<int>::from(str, 100); // default supplied so no error } else { if (want the "class-defined" default) { // ie we want convert to default-construct the result if necessary int max = convert<int>::from(str, throw_ = false); // or ? int max = convert<int>::from(str); // ie what's the default for throw_? // or, of course int max = convert<int>::from(str, int()); } else // want errors { int m = convert<int>::from(str, throw_ = true); // or maybe int m = convert<int>::from(str, &error); } } } else // NOT DefaultConstructible { struct NoDefCon { NoDefCon(int x, int y) { } }; if (want specific default) { // supply default explicitly // same form as other specific-default case above NoDefCon ndc = convert<NoDefCon>::from(str, NoDefCon(1, 3)); } else { // don't want a specific default? // no choice but to throw: NoDefCon ndc = convert<NoDefCon>::from(str); } } If all the concepts were completely orthogonal, we'd have a few more if blocks.
I think that for these non Default constructible types you can use a trait that gives you a default value for types not having a default constructor. E.g.
template <typename T> struct default_value_trait { static const T value=T(); };
template <> struct default_value_trait<direction> { static const direction value=direction::up; };
So now you don't need any more to pass the default value as parameter, and the following should compile
direction dir = convert<direction>::from (str);
and could throw if conversion fails after minor modification on the implementation.
Yes, it is an interesting approach and I like it as it allows to deploy throwing 'convert' uniformly for DefaultConstructible and NonDefaultConstructible types. The only hesitation that pops to my mind is that I feel 'convert' is fairly straightforward and needs to be used without much/any preparation. (That's how I tend to use it anyway). That's why I was whining about the named parameters' interface as it requires a round trip to the top of the file to add 'using'. If lazy-me has to do the same for the default_value_trait<direction> specialization, then I'll probably try avoiding that. On the other hand, integration of a Type into the lexical_cast/convert framework does require some preparation -- namely op>>() and op<<(). We could keep it for backward compatibility but might offer a new trait-based way of integrating a Type into the lexical_cast/convert framework. Like
template<> struct convert_trait<Type> { }
where we might put anything related to the integration of the Type into the convert framework. Is it too far-fetched? Thinking aloud really.
I suppose you can reach the same effect with the error_value needed when the user want no_thow semantics.
template <typename T> struct error_value_trait { static const T value=T(); };
template <> struct error_value_trait<direction> { static const direction value=direction::down; };
So the following
direction dir = convert<direction>::from (str)(_thows=false);
will return direction::down when conversion fails after some modification on the implementation
That feels over-engineered. Somehow I feel more comfortable with more explicit
direction dir = convert<direction>::from(str, direction::dn);
One more question raise now. Should the fact the conversion throws or not when failing be part of the function type or a function parameter?
Do we need
direction dir = convert<direction, no_thows<> >::from (str);
or
direction dir = convert<direction>::from (str)(_thows=false);
Yes, that's a fair question. I considered convert<Type, Throw> before and back then it seemed that it was more flexible to configure the throwing behavior via a parameter. Having convert<Type>::result played a role as well.
To me, in principle, there is no much difference between supplying a parameter as a template or as an argument. So, I chose the one that looked less restrictive and scaled better. Do you feel the template-based could do better?
Resumein, I think these modifications are important, so the convert function can have always only the From parameter, and the throw semantics is preserved for non DefaultConstructible types.
From my usage pattern I do not think I want "the convert function to always have only the From parameter". In fact, truth to be told, for me it is the other way around -- I *never* use the convert function with only the From parameter. :-) I prefer having the failure value immediately visible as in the following (typical for my usage) example:
int v = convert<int>::from(s, MAX_INT); if (v == MAX_INT) conversion failed.
convert<direction>::result d = convert<direction>::from(s, direction::up); if (!d) conversion failed.
That said, convert_trait seems tempting. What others think?
V.
Overall, I think defaults are not class-level decisions, but call-level choices. Like the min/max case. One convert call might use 0 as the default, the other 100. I DO think that an error_value, IS (typically) a class-level trait. Well, maybe not for ints. Sometimes, in one context, 0 might be a good error value, sometimes -1, etc. Often it may be the case that need a stronger type than 'int', but that doesn't always happen in pratice. Could always pass in the trait: int x = convert<int, my_convert_traits>::from(str); with a default of convert_trait<To> if not specified. Basically, I'm not convinced about the traits... Tony