1
\$\begingroup\$

I recently had a bug where extracting a decimal from a string failed due to locale settings. That is, some locales use a , as a decimal point, rather than a .. An important goal is that the conversion function is deterministic.

I have usually used boost::lexical_cast for such tasks, but my understanding is that this is reliant on the global application locale. I have therefore implemented a variant of lexical_cast that uses the std::locale::classic "C" locale for the conversion.

#include <type_traits>
#include <locale>
#include <string>
#include <sstream>
#include <stdexcept>

namespace typeconv
{

/**
 * Convert @c str to a T
 *
 * @param str string to convert to a T
 * @return the value contained within @c as a T
 *
 * @pre @c str is arithmetic and can be converted to a T
 * @note std::locale::classic() is used for the conversion.
 *
 * @throws std::invalid_argument if str cannot be converted to an object of type T.
 */
template<typename T>
inline auto lexical_cast(const std::string& str)
    -> typename std::enable_if<std::is_arithmetic<T>::value, T>::type
{
    std::istringstream istr(str);
    istr.imbue(std::locale::classic());

    T val;
    istr >> val;

    if (istr.fail())
        throw std::invalid_argument(str);

    return val;
}

}

I plan to extend the template function in future to include other conversions, such as an arithmetic type to std::string. Any comments on the implementation are welcome.

\$\endgroup\$
1
\$\begingroup\$

I see two things I'd change here.

The first is that right now it supports only conversion from string to the specified (arithmetic) type. Sometimes you want (for example) to convert to a string, not just from one. To support that, we can add a second template parameter for the type of the input argument.

My second big point is that you're starting from a problem where the locale to use was hard-wired into boost::lexical_cast. But instead of fixing (what seems to me like) the real problem, you've just hard-wired a different locale.

Instead of that, I'd prefer to specify a locale to use by default, but also allow the user to specify a locale to use for the conversion, if they choose to do so.

Putting those together, we get a function something like this:

template<typename T, typename U>
inline auto lexical_cast(U const& in, std::string const &name = "C")
{
    std::stringstream istr;
    istr.imbue(std::locale(name));
    istr << in;

    std::string str = istr.str(); // save string in case of exception

    T val;
    istr >> val;

    if (istr.fail())
        throw std::invalid_argument(str);

    return val;
}

Now we can do things like:

// Treat as German input
auto x = lexical_cast<double>("1.234,567", "de");

or:

// convert to string in user's locale
auto s = lexical_cast<std::string>(123456.78, "");

Finally, I'd note one basic difference between this and boost::lexical_cast. This throws an exception if and only if it is unable to convert the input to a result of the output type (at all). Boost lexical_cast not only requires that you be able to convert the input to the output type, but also that when the conversion completes that it consume the entire input.

Consider, for example, something like:

int x = lexical_cast<int>("1 x");

With your lexical_cast, this will succeed because it successfully converted the 1 to an int. With Boost lexical_cast, this will throw an exception because even though it converted 1 to an int, that didn't use the x.

\$\endgroup\$
  • \$\begingroup\$ Sure, adding a parameter to specify the locale is a useful enhancement (this is essentially what the iostreams do with imbue). The problem is not that boost::lexical_cast is hard wired to a locale, it is rather that it uses the global application locale, which can be modified from anywhere within the application, making boost::lexical_cast non-deterministic. A function whose behaviour is dictated by a writable global variable is asking for trouble. The situation is even worse in a multi-threaded environment. Note that the printf and scanf family of functions have similar issues. \$\endgroup\$ – Frank Nov 11 '16 at 6:13
  • \$\begingroup\$ That is an interesting point that boost::lexical_cast requires all input to be consumed. I can see the benefit of being strict, as it forces the caller to ensure that the input is valid. I can also see the benefit of a conversion function that is more forgiving. \$\endgroup\$ – Frank Nov 11 '16 at 6:36
  • \$\begingroup\$ @Frank: Yes--I was careful to not call it a bug or defect. It would be if you wanted behavior as close to Boost lexical_cast as possible outside of its locale handling. OTOH, the behavior you have right now also strikes me as entirely reasonable. \$\endgroup\$ – Jerry Coffin Nov 11 '16 at 6:43
  • \$\begingroup\$ To specify a locale as a default argument, I would use an actual std::locale object, rather than a string, i.e. const std::locale& loc=std::locale::classic(). Strings naming locales are not guaranteed to be platform independent. \$\endgroup\$ – Frank Nov 11 '16 at 6:45
  • \$\begingroup\$ @Frank: that was how I originally wrote the code, but after some thought I decided it worked better this way. The basic problem is that although strings aren't platform independent, it's pretty much all we've got. So the only question is whether you have one copy of code to take a string and build a locale, or require that the user duplicate that code every place a different locale is needed. Taking a locale instead of a sting doesn't really fix the problem--just shifts the burden from me to the user (the opposite of what I want from a library, as a rule). \$\endgroup\$ – Jerry Coffin Nov 11 '16 at 15:53

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.