Search code examples
c++templates

using templates to prevent narrowing but allow widening of inputs


I have a class template, intended to be templated on floating points. The class has a method that should allow the inputs to be widened if needed (eg float -> double), but narrowing (ie- loss of precision) should not be allowed. How would I go about enforcing this?

The following example is the best I've managed to do (using if constexpr, so at least c++17), but it feels to me that using both a not-implemented specialization and a static_cast is probably not the cleanest, clearest solution. It actually does the job as described, provided only floats and doubles are used (as opposed to more esoteric floating point types), which is not an issue for me:

#include <vector>
#include <type_traits>


template <typename Real>
class Foo
{
public:
    template <typename Real2>
    void DoSomething( const std::vector<Real2>& v );
};


// Enter here only when input is of same type as template
template <typename Real>
template <typename Real2>
void Foo<Real>::DoSomething( const std::vector<Real2>& v)
{
    if constexpr ( !std::is_same<Real, Real2>::value )
        static_assert(false);
    
    // ... Do actual work here ...
}


// Allow widening of inputs
template <>
template <>
void Foo<double>::DoSomething( const std::vector<float>& v)
{
    std::vector<double> v2;
    v2.reserve( v.size() );
    for ( const auto& val : v )
        v2.push_back( (double)val );
    DoSomething(v2);
}


// Deny narrowing of inputs
template <>
template <>
void Foo<float>::DoSomething( const std::vector<double>& v);


int main()
{
    std::vector<float> v1;
    std::vector<double> v2;

    // ... populate both v1 & v2 ...

    Foo<float> b1;
    b1.DoSomething( v1 );
    //b1.DoSomething( v2 );  // <--- This line should not be allowed to compile

    Foo<double> b2;
    b2.DoSomething( v1 );
    b2.DoSomething( v2 );
}

How could I simplify the implementation to achieve the stated goal in a cleaner way?

Edit: changed the incorrect terms 'Upcasting' & 'Downcasting' to 'Widening' & 'Narrowing'

Edit2: somehow I marked the wrong line expected not to compile.


Solution

  • You can do this in many ways, for example with concepts:

    #include <vector>
    
    template <typename Real>
    class Foo
    {
    public:
        template <typename Real2>
        requires requires (Real r) {Real2{r};} // <-- here
        void DoSomething( const std::vector<Real2>& v );
    };
    

    Or, with a named concept producing a clearer error message:

    template <typename A, typename B>
    concept is_not_narrower = requires(B b) {A{b};};
    
    template <typename Real>
    class Foo
    {
    public:
        template <typename Real2>
        requires is_not_narrower<Real2, Real>
        void DoSomething( const std::vector<Real2>& v );
    };
    

    Live demo.