Can optional<double> be implemented as 8-byte object?
Asked Answered
T

4

6

Is it possible to implement std::optional such that sizeof(std::optional<double>) == 8 by somehow using that you can store characters in a NAN, see http://en.cppreference.com/w/cpp/numeric/math/nan? Are there implementations that do that? Can it be done in terms of the functionality that is available in the standard?

Trichoid answered 4/8, 2017 at 15:39 Comment(7)
double is not guaranteed to have a size of 8. For example some platforms use a 4 byte double. Perhaps the question should be worded as sizeof(std::optional<double>) == sizeof(double).Marni
The question is, do you want to know if this can be done well in practice so that it is reliable on some platform? Or do you want to know if it can be done in a way that is 100% standard compliant? The answer is yes and no.Subgenus
You can use std::isnan instead of has_value and you have what you want.Peavy
@NirFriedman I guess I would like to know both. I was just wondering if this technique can be leveraged somehow. Are there implementations that do that? Is it possible in implementations that are based on the IEEE standard?Trichoid
@TobiasBrüll We didn't do it in the contest of 'optional' but I've worked on a library which layered an "NA" value (similar to what R/pandas do) onto the primitives. For signed integer types it used the most negative value, and for floating point it used one of the quiet NaN bit patterns. So it's definitely practical, and it works well. But it can't be done by the standard, afaik there is no bit pattern that's illegal. Some are NaN, some are sub-normal, but they are all legal.Subgenus
You might be interested in compact_optional.Coblenz
This duplicate question has a good answer on how exactly the standard prohibits this idea.Emergence
W
1

I don't think this can be done because there is no rule preventing programs from utilizing and relying on the extra bits in NaN on their own. Then if you store the magic number into the optional it looks like it's not present instead of the application's special NaN.

Weimaraner answered 4/8, 2017 at 15:47 Comment(1)
What if the magic number was not a legal double value? There's no rule that says that every possible combination of bit values that can fit in a double must represent a legal double value. Fault bit patterns are allowed and if there's a reason optional couldn't use them, your answer doesn't explain what it is.Barbate
F
1

Answer is multifold.

First of all, it can not be implemented with the functionality available in Standard, since Standard says nothing of floating point implementation.

Second, for IEEE 754 floating points you can implement your own optional by specializing std::optional for doubles. However, this would mean that you exclude a valid value (NaN is a result produced by some arithmetic operations) from your range of values. However, diving deep into IEEE 754, you might choose a specific NaN representation (there are a lot of those!) as a no-value.

Flyman answered 4/8, 2017 at 15:47 Comment(3)
Doesn't the standard disallow specializing standard templates unless at least one argument is a user defined type? Also, the specialization would still be required to meet the requirements of std::optional.Mackey
@user2079303 Yes it does. specializing optional for double would be UB. However, you can create your own type that contains a double, and implicitly converts to it and use that. Probably annoying in some situations though (template edge cases).Subgenus
@user2079303 Well, at this time your code is not portable anyways, so I would not care too much about generic standard problems. It is prohibited simply because implementations are allowed to specialize (or overload in case of functions) for non-user-defined types, so it would be enough to verify your implementation doesn't do this for optional<double> and move on.Flyman
M
1

It is not possible to implement std::optional like that because it contradicts the post-conditions that specify how the class (template) behaves. For example: std::optional contains a value if it is initialized with a value of type T, but your suggested std::optional<double> would not contain a value if it was initialized with a value that is the special NaN value that you've chosen.

Also, the C++ standard does not guarantee/require that the floating point type supports (quiet) NaN. Some systems do not.

It is certainly possible to implement your own non-standard optional class with different semantics. Of course, you will then be relying on the implementation defined fact that NaN values exist. You also have to rely on the knowledge of the floating point representation, because as far as I know, there are no standard utilites for inspecting the NaN payload - only for generating a value that has a specific payload.

Mackey answered 4/8, 2017 at 15:48 Comment(3)
What if the special NaN value was not a legal value for a double but still fits. For example, imagine a platform where every double had an extra "is a valid double" bit that was used internally only for debug purposes. Why couldn't optional co-opt this bit?Barbate
@DavidSchwartz I suppose, if the system reserved this bit specifically for the compiler to do as it wishes. And I suppose the possibility is largely academical :)Mackey
How sure are you that every possible bit pattern corresponds to a double value that it's possible to produce? All you need is one pattern that doesn't.Barbate
S
0

Implementing what you propose is rather trivial, using a good text editor and cut-and-paste. Since it's a good idea, I've decided to add it to my tool box. My main motivation is that std::optional<>s are rather big, and thus not practical to use in std::variant<> types.

#include <type_traits>
#include <limits>
#include <exception>

class bad_optional_flt_access
    : public std::exception
{
public:
    bad_optional_flt_access() {}

    const char* what() const noexcept override 
    { 
        return "bad optional float access"; 
    }
};

template <typename Float, bool = std::is_floating_point<Float>::value>
class optional_flt;

template <typename Float>
class optional_flt<Float, false> {};

template <typename Float>
class optional_flt<Float, true> 
{
public:
    constexpr optional_flt() noexcept : value_(std::numeric_limits<Float>::quiet_NAN()) {}
    constexpr optional_flt(const Float& val) noexcept : value_(val) {}

    template<typename T>
    constexpr optional_flt(const T& val) noexcept : value_(Float(val)) {}

    constexpr bool has_value() const noexcept
    { 
        return value_ != std::numeric_limits<Float>::quiet_NAN(); 
    }

    void reset() noexcept { value_ = std::numeric_limits<Float>::quiet_NAN(); }

    constexpr void swap(optional_flt& other) noexcept { std::swap(value_, other.value_); }

    constexpr operator bool() const noexcept { return has_value(); }

    Float& value () &
    {
        if (!has_value())
           throw bad_optional_flt_access(); 
        return value_; 
    }

    Float&& value () &&
    {
        if (!has_value())
           throw bad_optional_flt_access(); 
        return value_; 
    }

    constexpr const Float& value () const &
    {
        if (!has_value())
           throw bad_optional_flt_access(); 
        return value_; 
    }

    Float& operator * () & noexcept { return value_; }
    constexpr const Float& operator * () const & noexcept{ return value_; }

    template< class U >
    constexpr Float value_or( U&& default_value ) const&
    {
        return (has_value()) ? value_ : default_value;
    }

    template< class U >
    constexpr Float value_or( U&& default_value ) &&
    {
        return (has_value()) ? value_ : default_value;
    }

private:
    Float value_;
};

template< class T, class U >
constexpr bool operator==( const optional_flt<T>& lhs, const optional_flt<U>& rhs )
{
    return lhs.value() == rhs.value();
}

template< class T, class U >
constexpr bool operator!=( const optional_flt<T>& lhs, const optional_flt<U>& rhs )
{
    return lhs.value() != rhs.value();
}

template< class T, class U >
constexpr bool operator<( const optional_flt<T>& lhs, const optional_flt<U>& rhs )
{
    return lhs.value() < rhs.value();
}

template< class T, class U >
constexpr bool operator<=( const optional_flt<T>& lhs, const optional_flt<U>& rhs )
{
    return lhs.value() <= rhs.value();
}

template< class T, class U >
constexpr bool operator>( const optional_flt<T>& lhs, const optional_flt<U>& rhs )
{
    return lhs.value() > rhs.value();
}

template< class T, class U >
constexpr bool operator>=( const optional_flt<T>& lhs, const optional_flt<U>& rhs )
{
    return lhs.value() >= rhs.value();
}

template<typename T>
constexpr optional_flt<T> make_optional_flt(const T& x)
{
    return optional_flt<T>(x);
}

int main()
{
    int i = 2;

    auto x = optional_flt<float>{i};
    auto y = optional_flt<double>(2.5);


    return (*x < .5) ? sizeof(optional_flt<double>) : 1;
}

The code above is gcc -std=c++11, clang -std=c++14, and cl /std:c++11 compatible.

Strobilaceous answered 13/8, 2017 at 15:49 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.