5

I want to have a class representing a unit with some kind of dimension. This should express something like 1.5m^2. A scalar multiplication with some type shall be allowed and a dimensionless unit should behave exactly like the underlying type. Here is my solution:

#include <type_traits>

template<typename T, int Dim>
class Unit {
    public:
    explicit Unit(T t): _value(t) {}

    template<int D = Dim, typename std::enable_if_t<D==0, int> = 0>
    operator T() { static_assert(Dim==0, ""); return _value; } //static_assert not necessary, but gives error if template is removed
    T _value;
};

template<typename S, typename T, int Dim>
auto operator*(S s, Unit<T,Dim> unit)
{
    return Unit<T, Dim>(s * unit._value);
}

auto main() -> int
{
    auto i = double{0};

//Scalar test
    auto scalar = int{0};
    auto x = Unit<double,1>(i);
    auto test = scalar * x;

//Conversion test
    auto y = Unit<double,0>(i);
    return y + i;
}

This works perfectly fine in clang (https://godbolt.org/z/8Pev7W6Y1). However, due to a GCC bug with templated conversion operators (Conversion operator: gcc vs clang), this does not work in GCC.

It is not possible to remove the SFINAE construction because it (correctly) runs into the static_assert.

Do you have an idea for equivalent code that also works in GCC? The code should work in C++17 with both compilers.

Henk
  • 826
  • 3
  • 14
  • Can you show an example of the code you would like to compile, but it fails to compile if the template is removed? If I remove the template from the godbolt link, both gcc and clang compile the code in the link – NathanOliver Nov 30 '21 at 13:48
  • In C++20, `operator T() requires (Dim == 0)` would do the job [Demo](https://godbolt.org/z/K6rzd8Md9). – Jarod42 Nov 30 '21 at 13:50
  • @NathanOliver: I think it is the opposite `static_cast(Unit(0))` would incorrectly compile without the SFINAE (and the `static_assert`) – Jarod42 Nov 30 '21 at 13:53
  • @Jarod42 I get that, but if they leave the static assert in, it will still fail to compile. I'm trying to understand *It is not possible to remove the SFINAE construction because it (correctly) runs into the static_assert*. I'm not sure what that means – NathanOliver Nov 30 '21 at 13:55
  • You're right, I have messed up the minimal example (and failed and checking). The problem in my real example is that the conversion operator is tried before the given `operator*` and then runs into the `static_assert`. I am trying to fix the example. – Henk Nov 30 '21 at 14:01
  • Not sure if it is applicable to the use-case, but the [Boost Units](https://www.boost.org/doc/libs/1_77_0/doc/html/boost_units.html) could be of interest. – Eljay Nov 30 '21 at 14:21

2 Answers2

4

You can use specialization instead of SFINAE. To avoid too much duplication you can move the common parts (anything that does not depend on Dim) to a base class:

#include <type_traits>

template <typename T>
class base_unit {
    public:
    explicit base_unit(T t): _value(t) {}
    T _value;
};


template<typename T, int Dim>
class Unit : public base_unit<T> {
public:
    explicit Unit(T t): base_unit<T>(t) {}
};

template <typename T>
class Unit<T,0> : public base_unit<T> {
public:
    explicit Unit(T t) : base_unit<T>(t) {}
    operator T() { return base_unit<T>::_value; }
};

template<typename S, typename T, int Dim>
auto operator*(S s, Unit<T,Dim> unit)
{
    return Unit<T, Dim>(s * unit._value);
}

auto main() -> int
{
    auto i = double{0};

//Scalar test
    auto scalar = int{0};
    auto x = Unit<double,1>(i);
    auto test = scalar * x;

//Conversion test
    auto y = Unit<double,0>(i);
    return y + i;
}

Live Demo

Note that this is a little old-fashioned and does not consider more modern C++20 approaches (for example the operator T() requires (Dim == 0) mentioned in a comment).

463035818_is_not_an_ai
  • 109,796
  • 11
  • 89
  • 185
0

Do you have an idea for equivalent code that also works in GCC? The code should work in C++17 with both compilers.

Since you don't want the code on the call side to be changed and the problem is with y+i, you could overload operator+ as shown below:

#include <type_traits>
#include <iostream>
template<typename T, int Dim>
class Unit {
    public:
    explicit Unit(T t): _value(t) {}

    template<int D = Dim, typename std::enable_if_t<D==0, int> = 0>
    operator T() { static_assert(Dim==0, ""); return _value; } //static_assert not necessary, but gives error if template is removed
    T _value;
    //overload operator+
    template<typename U,int D, typename V>  friend U operator+( Unit<U,D>& u,  V& v);  
};
//define overloaded operator+
template<typename U, int D, typename V> U operator+( Unit<U,D>& u,  V&v)
{
    std::cout<<u.operator U() + v;//just for checking the value
    return u.operator U() + v;
}
template<typename S, typename T, int Dim>
auto operator*(S s, Unit<T,Dim> unit)
{
    return Unit<T, Dim>(s * unit._value);
}

auto main() -> int
{
    auto i = double{0};

//Scalar test
    auto scalar = int{0};
    auto x = Unit<double,1>(i);
    auto test = scalar * x;

//Conversion test
    auto y = Unit<double,0>(i);
    
    return y + i;
}

The output the above program can be seen here.

Jason
  • 36,170
  • 5
  • 26
  • 60
  • 2
    I think OP wants implicit conversions to `T` (where explicitly calling the converision operator doesn't help). I mean "stating the obvious" is good, but I am not sure if this is what OP calls "equivalent code" – 463035818_is_not_an_ai Nov 30 '21 at 13:58
  • 1
    Yes, the code should not be changed on the calling side. – Henk Nov 30 '21 at 13:59
  • @463035818_is_not_a_number But OP didn't mention this important thing in his/her question. – Jason Nov 30 '21 at 13:59
  • not sure if you got my point. OPs conversion operator is implicit. While your answer is a workaround it isnt one for implicit conversions. – 463035818_is_not_an_ai Nov 30 '21 at 14:00
  • @AnoopRana Actually the OP did. They said *Do you have an idea for equivalent code that also works in GCC* which to me reads I don't want to change the driver code, only the code it calls – NathanOliver Nov 30 '21 at 14:01
  • @NathanOliver That is your interpretation of the quoted statement. – Jason Nov 30 '21 at 14:03
  • @Henk I have edited my answer to use overloaded `operator+` which needs no change on the caller side. Check it out. – Jason Nov 30 '21 at 14:40
  • @463035818_is_not_a_number I have edited my answer to use overloaded `operator+` which needs no change on the caller side. Check it out. – Jason Nov 30 '21 at 14:44
  • You would need this overloading for every possible situation to get implicit conversion with this approach in gcc. – Henk Nov 30 '21 at 14:45
  • @Henk yes that is the downside. For example, for `y * i` or `y - i` to work you/we would have to overloaded the respective operators as well. But still it solves(or is one possible way of solving) the current problem. – Jason Nov 30 '21 at 15:02