4

Just for sake of example, let's say I have a function that computes the average value of a given std::vector<T>:

    template<class T>
    T vec_average(const std::vector<T>& vec) {
      assert(!vec.empty());
      T accumulator = vec[0] - vec[0]; // Init to 0.
      for (const auto& el : vec) { accumulator += el; }
      return accumulator / double(vec.size());
    }

Now of course the requirement is that all the operators (+=, /, -) are defined for T and there might be rounding issues, but the more severe problem is that calling vec_average with a "small type" like uint8_t will quickly lead to bad results due to overflow.

type_traits to the rescue! you might say and indeed that is my initial attempt at solving this:

    template<class T>
    struct safe_accum {
      typedef typename std::conditional<std::is_integral<T>::value, int64_t, T>::type type;
    };

    template<class T>
    T vec_average(const std::vector<T>& vec) {
      assert(!vec.empty());
      safe_accum<T>::type accumulator = vec[0] - vec[0]; // Init to 0.
      for (const auto& el : vec) { accumulator += el; }
      return accumulator / double(vec.size());
    }

But: This only solves it for all the integer and floating point types. As soon as I have a struct or class type this breaks down (because std::is_integral doesn't work on class types):

    struct Vector3uc {
      uint8_t data[3];
      // Operator definitions etc...
    };

    void foobar() {
      std::vector<Vector3uc> bla;
      // ...
      Vector3uc avg = vec_average(bla); // Won't work!
    }

Therefore, my question is: Can I have a safe_accum type that works for "classy" input types T as well?

For example I'd like to tell the compiler that a safe_accum<Vector3uc>::type would have to result in a Vector3ll (a 3-vector of int64_t), whereas a safe_accum<Vector3f>::type would simply be a Vector3f.

I'm pretty sure it can be done with typedefs and/or template specialization but this is a bit of a grey area in my knowledge of C++...

PS: Just to be clear I wouldn't mind defining these translations (Vector3uc --> Vector3ll) manually in the code for each relevant type, as long as it's only a line or two per type.

Asteroids With Wings
  • 17,071
  • 2
  • 21
  • 35
rsp1984
  • 1,877
  • 21
  • 23
  • I don't think it's actually possible, but if there is a way, I'd love to hear it too! – Toby Speight Feb 07 '20 at 12:48
  • 2
    Why not simply specialize `safe_accum` for your type? – Holt Feb 07 '20 at 12:53
  • `operator+` is defined by `Vector3uc`. How should a template modify it when it doesn't know anything about `Vector3uc` semantically. This is the responsibility of `Vector3u` or you need to restrict your template to more specialized classes, e.g a `Vector` protocol with clearly defined semantics (e.g required typedefs) – Sebastian Hoffmann Feb 07 '20 at 12:53
  • I initially wrote this answer here but I realized it is rather off-topic. It is still relevant: https://stackoverflow.com/a/60115962/3052438 – Piotr Siupa Feb 07 '20 at 15:03

2 Answers2

3

You might give more customization point to your traits:

template <class T, typename Enabler = void>
struct safe_accum
{
    using type = T;
};

template <class T>
struct safe_accum<T, std::enable_if_t<std::is_integral<T>::value>>
{
    using type = int64_t;
};

class Vector3uc;
class Vector3ll;

template <>
struct safe_accum<Vector3uc>
{
    using type = Vector3ll;
};
// ...
Jarod42
  • 203,559
  • 14
  • 181
  • 302
  • *"defining these translations [..] as long as it's only a line or two per type"*. Ok, it is five lines here... ;-) – Jarod42 Feb 07 '20 at 12:58
  • Cool! But what about definition order? So for example I'd like to keep all the specialized "translation tables" for safe_accum in the headers of the types and not where I define the generic safe_accum. Is that possible without creating weird header-inclusion-order problems? – rsp1984 Feb 07 '20 at 13:04
  • You might do it in only one header, but you have to forward declare the class. If you use SFINAE, then you probably need include. – Jarod42 Feb 07 '20 at 13:13
0

You can introduce a type trait that would be used by safe_accum to obtain the intermediate accumulator type.

template< typename T >
struct accumulator_type;

template< >
struct accumulator_type< float > { typedef float type; };

template< >
struct accumulator_type< double > { typedef double type; };

template< >
struct accumulator_type< char > { typedef int type; };

template< >
struct accumulator_type< unsigned char > { typedef unsigned int type; };

// etc. - you have to specialize it for every type you want to accumulate

template<class T>
T vec_average(const std::vector<T>& vec) {
    assert(!vec.empty());
    typename accumulator_type<T>::type accumulator{}; // Init to 0.
    for (const auto& el : vec) { accumulator += el; }
    return accumulator / double(vec.size());
}

You can use tools like Boost.Integer to automatically select larger integer types for the accumulator in accumulator_type implementation.

Andrey Semashev
  • 10,046
  • 1
  • 17
  • 27