Search code examples
c++implicit-conversionconstexprtype-traitsif-constexpr

How can I prevent implicit conversions in std::is_constructible


Let's say, I have a few different classes:

class constructible_from_float {
public: 
    constructible_from_float(float);
};
class constructible_from_double {
public: 
    constructible_from_double(double);
};
class constructible_from_long_double {
public: 
    constructible_from_long_double(long double);
};

And then I want to do something based on what type are they constructible from (simplified example):

#include <type_traits>
template <typename T>
constexpr size_t foo() {
    if constexpr (std::is_constructible<T, float>::value) {
        return 1;
    } else if constexpr (std::is_constructible<T, double>::value) {
        return 2;
    } else if constexpr (std::is_constructible<T, long double>::value) {
        return 3;
    } else
        return -1;
}

But the problem is, all of these return 1:

[[maybe_unused]] auto float_result = foo<constructible_from_float>();
[[maybe_unused]] auto double_result = foo<constructible_from_double>();
[[maybe_unused]] auto long_double_result = foo<constructible_from_long_double>();

enter image description here

I'm aware that the reason for the behavior is implicit conversions between types. Is there a legit (usable on at the very least three major compilers: msvc, gcc and clang) way to force the compiler to distinguish between these types.

I'm not allowed to to change the classes (constructible_from_float, etc.) but can do everything else. Anything provided by the stable versions of the compilers is ok (including c++2a).


Solution

  • You have to fool the C++ compiler in revealing to you what implicit conversion it wants to use, then use SFINAE to pull the rug from under its feet, and fail to instantiate the template, but SFINAE, so it's not an error.

    #include <type_traits>
    #include <iostream>
    
    class constructible_from_float {
    public:
        constructible_from_float(float);
    };
    class constructible_from_double {
    public:
        constructible_from_double(double);
    };
    class constructible_from_long_double {
    public:
        constructible_from_long_double(long double);
    };
    
    
    template<typename T> class convertible_only_to {
    
    public:
        template<typename S, typename=std::enable_if_t<std::is_same_v<T,S>>>
        operator S() const
        {
            return S{};
        }
    };
    
    
    template <typename T>
    constexpr int foo() {
        if constexpr (std::is_constructible<T,
                  convertible_only_to<float>>::value) {
                return 1;
        } else
        if constexpr (std::is_constructible<T,
                  convertible_only_to<double>>::value) {
                return 2;
            } else
        if constexpr (std::is_constructible<T,
                  convertible_only_to<long double>>::value) {
                return 3;
        } else
            return -1;
    }
    
    struct not_constructible_from_anything {};
    
    int main()
    {
        std::cout << foo<constructible_from_float>() << std::endl;
        std::cout << foo<constructible_from_double>() << std::endl;
        std::cout << foo<constructible_from_long_double>() << std::endl;
        std::cout << foo<not_constructible_from_anything>() << std::endl;
    
        return 0;
    }