Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Can I detect an integer type without listing all of them as template specializations?

I am playing around with implementing C++ std library traits. I could implement is_integral like this:

template <typename T>
is_integral
{
   static constexpr bool value = false;
}
template <>
is_integral<int>
{
   static constexpr bool value = true;
}
// do this for like 10 other types, at least

I was trying to detect integers with reasonable reliability using two things:

  • (T(3)/T(2)) == T(1)
  • (T(0) & T(1)) == T(0)

Other could be added. But I am having trouble templating this. For this to compile, I need to avoid errors when / or & are not defined for T.

I tried this:

#if __cplusplus > 199711L
namespace impl
{
  // type that is returned for X op Y when operator op is missing
  struct NoOperator {}; 
  // Out of line operator for any X & Y
  template<typename T, typename Arg> NoOperator operator& (const T&, const Arg&);

  template<typename T, typename Arg = T>
  struct BitAndExists
  {
    // check if the result of bit and is our NoOperator type
    enum { value = !is_same<decltype(declval<T>() & declval<Arg>()), NoOperator>::value };
  };  
}
#endif

namespace impl
{
    template <typename T>
    constexpr bool has_integer_bit_and()
    {
        if constexpr (impl::BitAndExists<T>::value && !is_floating_point<T>::value)
        {
            return (T(0) & T(1)) == T(0);
        }
        else
        {
            return false;
        }
    }
    template <typename T>
    struct integral_checks {
        static constexpr bool bit_and_check = has_integer_bit_and<T>();
        static constexpr bool round_check = (T(3)/T(2)) == T(1);

        static constexpr bool value = bit_and_check && round_check;
    };
    template <typename T>
    struct integral_checks<T*> {
        static constexpr bool bit_and_check = false;
        static constexpr bool round_check = false;
        static constexpr bool value = false;
    };
    template <typename T>
    struct integral_checks<T&> {
        static constexpr bool bit_and_check = false;
        static constexpr bool round_check = false;
        static constexpr bool value = false;
    };
}

template<typename T>
struct is_integral
{
    static constexpr bool value = impl::integral_checks<T>::value;
};

But I am getting multiple errors:

error: invalid operands of types 'float' and 'float' to binary 'operator&'
  101 |     enum { value = !is_same<decltype(declval<T>() & declval<Arg>()), NoOperator>::value };

So this means the operator trick didn't work as intended.

Constexpr variable 'bit_and_check' must be initialized by a constant expression

This is more confusing, I'd used assignment from constexpr () to constexpr variable without problems, even with complex types.

Here is full code sample: https://godbolt.org/z/Ys9bqKq4a

Note that I am doing this as an exercise. I understand that duck-typing an int like this could have some nasty unintended consequences. But I want to learn from the errors above.

like image 756
Tomáš Zato - Reinstate Monica Avatar asked Sep 19 '25 00:09

Tomáš Zato - Reinstate Monica


2 Answers

The problem of your code is that your struct BitAndExist

template<typename T, typename Arg = T>
struct BitAndExists
{
  // check if the result of bit and is our NoOperator type
  enum { value = !std::is_same<decltype(std::declval<T>() & std::declval<Arg>()), NoOperator>::value };
};

requires (decltype(std::declval<T>() & std::declval<Arg>()) that the operator & between a T value and an Arg value is defined. Otherwise you obtain an error.

You have to place the same expression, or something similar, in a context where you can use SFINAE (Substitution Failure Is Not An Error). For example, in a template method, as follows

template <typename T>
struct is_integral
{
  template <typename U>
  static constexpr decltype((U(3) / U(2)), (U(1) & U(0)), bool{}) check (int)
  { return ((U(3) / U(2)) == U(1)) && ((U(0) & U(1)) == U(0)); }

  template <typename>
  static constexpr bool check (long)
  { return false; }

  static constexpr bool value = check<T>(0);
};

int main ()
{
  static_assert(!is_integral<float>::value, "is_integral detection error");
  static_assert(!is_integral<bool*>::value, "is_integral detection error");
  static_assert(!is_integral<bool&>::value, "is_integral detection error");
  static_assert(is_integral<int>::value, "is_integral detection error");
  static_assert(is_integral<char>::value, "is_integral detection error");
}

like image 82
max66 Avatar answered Sep 20 '25 16:09

max66


Since errors in your attempt were already addressed, I just wanted to add 2c about an alternative approach. You don't need to list all integer types or even duck type them too much. All fundamental integer types have an interesting property in common, they can be reinterpret_casted to an object-pointer type. We can use expression-sfinae on such a cast, which leaves us with something like this

#include <type_traits>
#include <utility>

template<typename T, typename = void>
struct is_integral : std::false_type {};

template<typename I>
struct is_integral<I, std::enable_if_t<
    !std::is_pointer_v<I> &&
    !std::is_reference_v<I> &&
    !std::is_same_v<I, void*> &&
    std::is_same_v<void*, decltype(reinterpret_cast<void*>(std::declval<I>()))>
>> : std::true_type {};


struct C {
    operator void*() const { return nullptr; }
};

int main() {
  static_assert(!is_integral<float>::value, "is_integral detection error");
  static_assert(!is_integral<bool*>::value, "is_integral detection error");
  static_assert(!is_integral<bool(*)()>::value, "is_integral detection error");
  static_assert(!is_integral<bool&>::value, "is_integral detection error");
  static_assert(!is_integral<int C::*>::value, "is_integral detection error");
  static_assert(!is_integral<C>::value, "is_integral detection error");   
  static_assert(is_integral<int>::value, "is_integral detection error");
  static_assert(is_integral<char>::value, "is_integral detection error");   
}

We basically vet that the type in question isn't one of the others that can pass the expression-sfinae of the cast (or a reference to one), then try to see if the cast expression is valid. Plugging this into your excellent test suite, that I extended a bit, is encompassing enough as far as I can tell.


As pointed out to in the comments bellow, I forgot enumerations are also subject to the same reinterpret_cast behavior. While I can easily address it by adding !std::is_enum_v<I> to the condition, that traits requires compiler magic (unlike all the others we used so far, that are implementable in regular code if one wises to avoid standard type traits). YMMV, depending on how you wish to approach this.

like image 44
StoryTeller - Unslander Monica Avatar answered Sep 20 '25 14:09

StoryTeller - Unslander Monica