Search code examples
c++template-specialization

Is it guaranteed that a specialization of std::numeric_limits<T> for user-defined numeric type S works for cv-qualified S out of the box?


I have a user-defined numeric type S for which I specialized std::numeric_limits<T>.

Although I specialized for S only, my custom max() is also used for cv-qualified S, at least with recent versions of gcc and MSVC.

Is this guaranteed to work, or am I relying on an implementation detail here?

#include <limits>
#include <iostream>

// Some user-defined numeric type,
// for which I'd like to specialize std::numeric_limits<T>.
struct S {};

namespace std
{
    // My specialization, for brevity providing only max()
    template <>
    struct numeric_limits<S>
    {
        static /*constexpr*/ S max()
        {
            std::cout << "got called" << '\n';
            return S();
        }
    };
}

int main()
{
    // Although I specialize for S only, my custom max() is also used for cv-qualified S.
    // Is this guaranteed, or am I relying on an implementation detail here? 
    std::numeric_limits<S>::max();                // Prints 'got called'
    std::numeric_limits<const S>::max();          // Prints 'got called'
    std::numeric_limits<volatile S>::max();       // Prints 'got called'
    std::numeric_limits<const volatile S>::max(); // Prints 'got called'
}

Solution

  • Yes, it's guaranteed since LWG559 which dealt with this specifically by requiring that

    • The value of each member of a numeric_limits specialization on a cv-qualified T is equal to the value of the same member of numeric_limits<T>.

    The proposed (and implemented) resolution was:

    • Add to the synopsis of the <limits> header, immediately below the declaration of the primary template, the following:

      template <class T> class numeric_limits<const T>;
      template <class T> class numeric_limits<volatile T>;
      template <class T> class numeric_limits<const volatile T>;
      

      These are for example implemented in gcc as such:

      template<typename _Tp>
        struct numeric_limits<const _Tp>
        : public numeric_limits<_Tp> { };
      
      template<typename _Tp>
        struct numeric_limits<volatile _Tp>
        : public numeric_limits<_Tp> { };
      
      template<typename _Tp>
        struct numeric_limits<const volatile _Tp>
        : public numeric_limits<_Tp> { };
      

    The LWG issue also has an informal note at the bottom:

    • [ Portland: Martin will clarify that user-defined types get cv-specializations automatically. ]

    You can see that the addition to the standard works as intended by implementing a similar type, here called foo:

    #include <iostream>
    
    // STD
    template<class T>
    struct foo {
        inline static constexpr bool is_S = false;
    };
    
    template <class T> struct foo<const T> : foo<T> {};
    template <class T> struct foo<volatile T> : foo<T> {};
    template <class T> struct foo<const volatile T> : foo<T> {};
    

    Now add a type, S, and a specialization for foo<S> only:

    // User-defined type and foo specialization:
    
    struct S {};
    
    template<>
    struct foo<S> {
        inline static constexpr bool is_S = true;
    };
    

    and foo<const volatile S> will pick the specialization:

    int main() {
        std::cout << foo<const volatile S>::is_S << '\n'; // prints 1
    }
    

    Demo