0

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 e.g. 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!

shevket
  • 163
  • 11
  • 1
    Regarding try 2: Templates are generally declared and defined in the header file else this might result in linker errors (more about it [here](https://stackoverflow.com/a/67170870/9938686)). – 2b-t Jul 04 '21 at 13:13
  • Your lambda needs an explicit `static_cast`. So either `[&] (size_t i) {return static_cast(i) / static_cast(n);}` or a templated lambda. Currently `u` is initialised with `0`. – 2b-t Jul 04 '21 at 13:35
  • You cannot override a non-virtual function. – n. m. could be an AI Jul 04 '21 at 13:38
  • @2b-t thanks for the pointer. Right for `static_cast` writing `(T)i / (T)n` also works – shevket Jul 04 '21 at 14:35
  • @shevket `(T)` is less expressive than `static_cast(...)`. `(T)` can mean very different things in different context: [See here](https://stackoverflow.com/questions/332030/when-should-static-cast-dynamic-cast-const-cast-and-reinterpret-cast-be-used) – 2b-t Jul 04 '21 at 16:45
  • @2b-t edited again for good practice :) – shevket Jul 06 '21 at 08:28

1 Answers1

1
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 Vectors:

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.

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
  • Thanks! I'll have to look into it deeper but I like the constructor signature requirement :) – shevket Jul 05 '21 at 14:44
  • @shevket Also consider `std::declval(*)(std::size_t)>()` -- that could avoid a compiler warning about "no return paths produce a value" or whatever. – Yakk - Adam Nevraumont Jul 05 '21 at 14:54
  • Could you provide a few hints about file splitting as well? Can I split `struct Vect {...}` and `template ...` in different .cpp or .hpp files? – shevket Jul 06 '21 at 08:37
  • I do get `error: no return statement in function returning non-void` with the `{ V([] (size_t) -> IndexResult{}) }` underlined. Could you be more precise about where to put `declval(*)(size_t)>()` and on the syntax of these constructs? Cheers! – shevket Jul 06 '21 at 10:21
  • Ok I updated the constructor requirement to `{ V (declval<...>()) }`. I put the concept and operator+ definition in a file "alg.h" and included it in a file "vect.h" defining the Vect class and it seems to work (not the otherway around, the concept should be imported from the instance). I don't know about best practices though... – shevket Jul 06 '21 at 11:03
  • @shevket What you describe seems reasonbale. – Yakk - Adam Nevraumont Jul 06 '21 at 13:18
  • as said I like the constructor constraint but what you to put in it remains quite opaque! Could you edit your answer with a few details so that I mark it accepted? (e.g. why your 1st suggestion doesn't compile, and what does the star mean in `Target(*)(Source)` when `Target(Source)` compiles as well inside the declval?) – shevket Jul 06 '21 at 21:12
  • @shev it didnpt compile because you have sensible warnings and warnings as errors on. The rest, function pointer, and decay of return values. – Yakk - Adam Nevraumont Jul 06 '21 at 23:29
  • Ok well I tried to compile the former without -Wall etc and it gave me lots of linker errors and "relocation against read-only section" blah blah. The thread is diverging a bit so I'll ask more specific questions later though. Thanks for the help! – shevket Jul 07 '21 at 10:14