2

I have two classes (this is a, extract from my actual more complex case)

class ClUInt {
public:
    ClUInt() : _i(2), _arr{1,2,3} {};
    ClUInt(const unsigned int ui) : _i(ui) {};
private:
    unsigned int _i;
    double _arr[3];
};

class ClInt {
public:
    ClInt() : _i(-2), _arr{-1,-2,-3} {};
    ClInt(const int i) : ClInt() { _i = i; };
private:
    int _i;
    double _arr[3];
};

They are very similar, but one uses int and the other unsigned int for member _i.

I want to overload operator<< with, e.g.,

std::ostream& operator<<(std::ostream& os, const ClInt & ci)
{
    cout << ci._i << endl;
    cout << ci._arr[0] << endl;
    return os;
}

Assume I want the "same" overload for both classes.

How can I write it only once, so it is easier to maintain? I thought of defining my own cast, but I am not sure it is the way to go...

Notes:

  1. I think I have no chance of making the two classes share any part of an inheritance tree.

  2. They could actually be structs, so if privacy affects the answer, one can assume _i and _arr are public.

  3. In the actual case, the two structs have a larger number of "common" members which are signed/unsigned, respectively.

3 Answers3

3

Use a template with concepts:

#include <concepts>
#include <iostream>

template<typename T>
concept ClInteger = std::same_as<T, ClInt> || std::same_as<T, ClUInt>;

template <ClInteger T>
std::ostream& operator<<(std::ostream& os, const T & ci)
{
    std::cout << ci._i << '\n';
    std::cout << ci._arr[0] << '\n';
    return os;
}

Note that this operator must be the friend of those classes to have access to their private fields.
LIVE

Aykhan Hagverdili
  • 28,141
  • 6
  • 41
  • 93
2

You could try creating a templated operator<< which works for only those two types using SFINAE. E.g.:

template <class T,
  std::enable_if_t<
      std::is_same<ClInt, std::decay_t<T>>::value
      or std::is_same<ClUInt, std::decay_t<T>>::value
    , int> = 0>
std::ostream & operator<< (std::ostream & out, T const & obj) {
  out << obj._i << '\n';
  out << obj._arr[0] << '\n';
  return out;
}

The above example is for C++14, but can be made to work with C++11 if you replace things like std::decay_t<T> with typename std::decay<T>::type.

Note: I replaced std::endl with '\n', since you probably don't want to be flushing your output stream every time. See C++: "std::endl" vs "\n".

AVH
  • 11,349
  • 4
  • 34
  • 43
  • is the `decay_t` necessary? – M.M Feb 16 '20 at 11:15
  • 2
    note that the SFINAE condition can be given its own name which will help with readability and reuse, i.e. `template using IsClXInt = std::enable_if_t< ..... >;` and then `template> std::ostream& operator<<` ... – M.M Feb 16 '20 at 11:18
  • 1
    @M.M I messed around in Godbolt a little and for the few cases I tried the `std::decay_t` seemed to be redundant indeed. I was thinking T could be e.g. `ClInt const &`, but it seems the reference and const "collapsing" is done before/during determining what "T" is exactly. – AVH Feb 16 '20 at 12:54
0

Maybe it's just coincidence, but your two classes share a lot. So, why not turn them into templates:

template<typename integer_type>
class IntegerClass
{
    /// ... further code elided

    friend ostream& operator<<(ostream& out, IntegerClass const& obj)
    {
        return out
            << obj._i << endl
            << obj._arr[0] << endl;
    }
    integer_type _i;
    double _arr[3];
};

// optional typedefs
// These are a matter of taste, not actually mine.
typedef IntegerClass<unsigned int> ClUInt;
typedef IntegerClass<signed int> ClInt;
Ulrich Eckhardt
  • 16,572
  • 3
  • 28
  • 55