简体   繁体   中英

Overload operators on different templated types with C++ concepts

I am trying to provide out-of-class definitions of arithmetic operators +-*/ (and in-place += etc.) for differently templated types. I read that C++20 concepts is the good way to go, as one could constrain the input/output type to provide only one templated definition, although I could not find much examples of this...

I am using a type-safe vector as base class:

// vect.cpp
template<size_t n, typename T> 
struct Vect {
    
    Vect(function<T(size_t)> f) {
        for (size_t i=0; i < n; i++) {
            values[i] = f(i);
        }
    }
    
    T values [n];

    T operator[] (size_t i) {
        return values[i];
    }
}

I have a derived class for tensors like so:

// tensor.cpp
template <typename shape, typename T>
struct Tensor : public Vect<shape::size, T> {
    // ... same initiliazer and [](size_t i)
}

and I shall also define a derived class for read-only views/slices, overriding operator [] to jump accross strides. I'd like to hard code little more than fmap and fold methods inside each class and avoid reproducing boilerplate code as much as possible.

I first had a bit of trouble coming up with a suitable concept for Vect<n,T> -like classes due to different template parameters, but the one below seems to work:

// main.cpp
template<typename V, int n, typename T> 
concept Vector = derived_from<V, Vect<n, T>>

template<int n, typename T, Vector<n, T> V>
V operator + (const V& lhs, const V& rhs) {
    return V([&] (int i) {return lhs[i] + rhs[i];});
}

int main () {
    size_t n = 10;
    typedef double T;
    Vect<n,T> u ([&] (size_t i) {return static_cast<T>(i) / static_cast<T>(n);});
    log("u + u", u);
    return 0;
}

Error: template deduction/substitution failed, could not deduce template parameter 'n'

Try 2:

Based on this question I figure out-of-class definition has to be a little more verbose, so I added a couple lines to vect.cpp .

This seems contrived as it would require (3 * N_operators) type signature definitions, where avoiding code duplication is what motivates this question. Plus I don't really understand what the friend keyword is doing here.

// vect.cpp
template<size_t n, typename T>
struct Vect;

template<size_t n, typename T> 
Vect<n, T> operator + (const Vect<n, T>& lhs, const Vect<n, T>& rhs);

template<size_t n, typename T>
struct Vect {
    ...
    friend Vect operator +<n, T> (const Vect<n, T>& lhs, const Vect<n, T>& rhs);
    ...
}

Error: undefined reference to Vect<10, double> operator+(Vect<10, double> const&, Vect<10, double> const&)' ... ld returned 1 exit status

I am guessing the compiler is complaining about implementation being defined in main.cpp instead of vect.cpp ?

Question: What is the correct C++ way to do this? Are there any ways to make the compiler happy eg with header files?

I am really looking for DRY answers here, as I know the code would work with a fair amount of copy-paste :)

Thanks!

template<int n, typename T, Vector<n, T> V>
V operator + (const V& lhs, const V& rhs) {
  return V([&] (int i) {return lhs[i] + rhs[i];});
}

here, you have to have a way to deduce n and T . Your V does not provide that; C++ template argument deduction does not invert non-trivial template constructs (because doing so in general is Halt-hard, it instead has a rule that this makes it non-deduced).

Looking at the body, you don't need n or T .

template<Vector V>
V operator + (const V& lhs, const V& rhs) {
  return V([&] (int i) {return lhs[i] + rhs[i];});
}

this is the signature you want.

The next step is to make it work.

Now, your existing concept has issues:

template<typename V, int n, typename T> 
concept Vector = derived_from<V, Vect<n, T>>

this concept is looking at the implementation of V to see if it is derived from Vect .

Suppose someone rewrote Vect with Vect2 with the same interface. Shouldn't it also be a vector?

Looking at the implementation of Vect:

Vect(function<T(size_t)> f) {
    for (size_t i=0; i < n; i++) {
        values[i] = f(i);
    }
}

T values [n];

T operator[] (size_t i) {
    return values[i];
}

it can be constructed from a std::function<T(size_t)> and has a [size_t]->T operator.

template<class T, class Indexer=std::size_t>
using IndexResult = decltype( std::declval<T>()[std::declval<Indexer>()] );

this is a trait that says what the type result of v[0] is.

template<class V>
concept Vector = requires (V const& v, IndexResult<V const&>(*pf)(std::size_t)) {
  typename IndexResult<V const&>;
  { V( pf ) };
  { v.size() } -> std::convertible_to<std::size_t>;
};

there we go, a duck-type based concept for Vector . I added a .size() method requirement.

We then write some operations on all Vector s:

template<Vector V>
V operator + (const V& lhs, const V& rhs) {
  return V([&] (int i) {return lhs[i] + rhs[i];});
}
template<Vector V>
std::ostream& operator<<(std::ostream& os, V const& v)
{
    for (std::size_t i = 0; i < v.size(); ++i)
        os << v[i] << ',';
    return os;
}

fix up your base Vect a tad:

template<std::size_t n, typename T> 
struct Vect {
    Vect(std::function<T(std::size_t)> f) {
        for (std::size_t i=0; i < n; i++) {
            values[i] = f(i);
        }
    }
    
    T values [n];

    T operator[] (std::size_t i) const { // << here
        return values[i];
    }
    constexpr std::size_t size() const { return n; } // << and here
};

and then these tests pass:

constexpr std::size_t n = 10;
typedef double T;
MyNS::Vect<n,T> u ([&] (size_t i) {return (T)i / (T)n;});
std::cout << "u + u" << (u+u) << "\n";

Live example .

(I used namespaces properly, because I feel icky when I don't).

Note that operator+ is found via ADL because it is in MyNS as is Vect . For types outside of MyNS you'd have to using MyNS::operator+ it into current scope. This is intentional and pretty much unavoidable.

(If you inherit from something in MyNS it will also be found).

...

TL;DR

Concepts should generally be duck typed , that is depend on what you can do with the type, not how the type is implemented. The code does not appear to care if you inherit from a specific type or template, it just wants to use some methods; so test that .

This also avoids trying to deduce the template arguments to the Vect class; we instead extract it from the interface.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM