EDIT(3): For whom it may concern in the future. I have abandoned the idea of using friend functions in conjunction with CRTP. Although the use of non-friend free function might incur additional levels of indirection, you can't (this I have learned the hard way) realistically expect CRTP to be an easy drop-in replacement for conventional dynamic polymorphism or overloading mechanisms. You will not get around understanding how to define and use user-defined type_traits, SFINAE, template instantiation orders and more - it's kind of an all or nothing deal and you will be forced to learn a lot, to put it optimistically.
I have updated the code example below. It shows the implementations of two algebraic operators operator+=()
and operator+()
, as well as a simple operator<<()
. In order to make the value_types of the derived classes known in the base class, I additionally defined a matrix_traits
class, which is specialized for the two derived classes respectively.
EDIT(2): As it turns out, the way I implemented friend operator<<()
like described below does not work after all, but will result in ambiguous overloads. With or without type_traits. I'm at my wit's end.
EDIT: After some investigation, I changed the title of this question and will describe the problem differently.
TL;DR:
- How to idiomatically use friend functions in the context of CRTP?
- When is SFINAE in combination with template functions/classes necessary?
What originally confused me was a different answer concerned with the implementation of operator+().
Since both the usage of friend functions with the CRTP and SFINAE can be tricky, it took me quite some time to figure out what the problems were and how to describe them.
I wanted to implement a Matrix class using the Curiously Recurring Template Pattern (CRTP) via deriving a "Dynamic_Matrix" and a "Static_Matrix" from a base class "Matrix_base". (I originally intended to both call them "Matrix", but this didn't turn out too well.)
Additionaly, I tried to implement operator+=()
, operator+()
(in terms of operator+=()
), and operator<<()
.
My original problem of a template parameter not being deduced was solved by overhauling and simplifying the template signatures. They are now:
template <typename MatrixType>
class Matrix_base;
template <typename T, template <typename, typename...> class Container = std::vector>
class Dynamic_matrix;
template <typename T, std::size_t Rows, std::size_t Columns>
class Static_matrix;
Problem 1: Friend functions.
A friend function of the base class is not friends with the derived classes.
The fact, that the base class itself is declared a friend in the derived class does not help here. On the contrary, clang++3.8.0 would compile an inline definition of friend std::ostream& operator<<()
in the base class, while g++4.9.2 (and higher) would not, due to member variables of the derived class being private. It took a while for me to notice that actually. In this case, it doesn't matter if a reference to the base class or the derived class (i.e. the template argument) is taken. Both work with clang++, but not g++.
What does work is forward declare operator<<, befriend a template specialization friend std::ostream& operator<<(std::ostream&, Derived const&);
in both derived classes respectively and define it below their function bodies. This is nothing unusual however, you see this often in combination with template classes and this brings me to the second "problem".
EDIT: Nope. If I try to std::cout << "text\n";
from inside a class member function, this will result in ambiguous calls to operator<<.
Problem 2: SFINAE in combination with free functions. Or rather, when to use it?
If Derived& operator+=(Derived const&)
is a public member function of the base class, we can implement operator+() as a free (non-friend) function of the signature
template <typename Derived>
Derived operator+(Derived lhs, Derived const &rhs);
or
template <typename Derived>
typename std::enable_if< std::is_base_of <Base <Derived>, Derived>::value, Derived>::type
operator+(Derived lhs, Derived const &rhs);
While the latter is probably more "correct", I wasn't able to use this SFINAE approach in combination with friend functions, since I can't forward declare this kind of function signature (type trait expression on incomplete type). So generally speaking, what trouble can I realistically get myself into if I weren't to use SFINAE for free template functions? Can I get into trouble with overload resolution? Or... ?
Any advice is appreciated!
Below is some functional code with a few comments:
Matrix.hpp:
#ifndef MATRIX_HPP
#define MATRIX_HPP
#include <array>
#include <string>
#include <cassert>
#include <vector>
#include <initializer_list>
#include <iterator>
#include <iostream>
#include <algorithm>
#include <utility>
#include <type_traits>
namespace mbw
{
// Forward declarations of Base and Derived types:
template <typename MatrixType>
class Matrix_base;
template <typename T, template <typename, typename...> class Container = std::vector>
class Dynamic_matrix;
template <typename T, std::size_t Rows, std::size_t Columns>
class Static_matrix;
// Forward declarations of Type traits:
template <typename MatrixType>
struct matrix_traits;
template <typename MatrixType>
class Matrix_base
{
public:
// make "value_type" known via an instance of template struct
using value_type = typename matrix_traits<MatrixType>::value_type;
std::size_t rows() const noexcept { return this->self()->rows_; }
std::size_t columns() const noexcept { return this->self()->columns_; }
value_type const* begin() const noexcept { return std::begin(this->self()->values_); }
value_type* begin() noexcept { return std::begin(this->self()->values_); }
value_type const* end() const noexcept { return std::end(this->self()->values_); }
value_type* end() noexcept { return std::end(this->self()->values_); }
bool operator==(Matrix_base const& rhs) { return this->self()->values_ == rhs.self()->values_; }
bool operator!=(Matrix_base const& rhs) { return this->self()->values_ != rhs.self()->values_; }
MatrixType& operator+=(MatrixType const&);
private:
MatrixType* self() noexcept { return static_cast<MatrixType*>(this); }
MatrixType const* self() const noexcept { return static_cast<MatrixType const*>(this); }
};
template <typename T, template <typename, typename...> class Container>
class Dynamic_matrix : public Matrix_base <Dynamic_matrix <T, Container>>
{
public:
using value_type = typename matrix_traits<Dynamic_matrix>::value_type;
Dynamic_matrix() = default;
Dynamic_matrix(std::size_t r, std::size_t c, std::initializer_list<T> const &list)
: rows_{r}, columns_{c}, values_(std::begin(list), std::end(list)) {}
template <typename InputIt>
Dynamic_matrix(InputIt first, InputIt last) : values_(first, last) {}
~Dynamic_matrix() = default;
Dynamic_matrix(Dynamic_matrix const&) = default;
Dynamic_matrix& operator=(Dynamic_matrix const&) = default;
Dynamic_matrix(Dynamic_matrix&&) = default;
Dynamic_matrix& operator=(Dynamic_matrix&&) = default;
private:
friend class Matrix_base <Dynamic_matrix <T, Container>>;
std::size_t rows_;
std::size_t columns_;
Container<T> values_;
};
template <typename T, std::size_t Rows, std::size_t Columns>
class Static_matrix : public Matrix_base <Static_matrix <T, Rows, Columns>>
{
public:
using value_type = typename matrix_traits<Static_matrix>::value_type;
Static_matrix() = default;
// Without this type trait expression, this constructor will always be preferred
// to the copy constructor:
template <typename... U, typename =
typename std::enable_if <!std::is_same <typename std::remove_reference <typename std::common_type
<U...>::type >::type, Static_matrix >::value >::type >
explicit Static_matrix(U&&... values) : values_{{std::forward<U>(values)...}} {}
template <typename InputIt>
Static_matrix(InputIt first, InputIt last)
{
#ifndef NDEBUG
if (static_cast<decltype(values_.size())>(std::distance(first, last)) > values_.size())
{
std::string msg{std::to_string(std::distance(first, last)) + " elements exceed maximum of "
+ std::to_string(values_.size())};
throw std::length_error{msg};
}
#endif
std::copy(first, last, begin(values_));
}
~Static_matrix() = default;
Static_matrix(Static_matrix const&) = default;
Static_matrix& operator=(Static_matrix const&) = default;
Static_matrix(Static_matrix&&) = default;
Static_matrix& operator=(Static_matrix&&) = default;
private:
friend class Matrix_base <Static_matrix <T, Rows, Columns>>;
static constexpr std::size_t rows_ = Rows;
static constexpr std::size_t columns_ = Columns;
std::array<T, Rows*Columns> values_;
};
// Specializations of type traits:
template <typename T, template <typename, typename...> class Container>
struct matrix_traits<Dynamic_matrix<T, Container>>
{
using value_type = T;
};
template <typename T, std::size_t Rows, std::size_t Columns>
struct matrix_traits<Static_matrix<T, Rows, Columns>>
{
using value_type = T;
};
// Implementations of member functions of Matrix_base:
template <typename MatrixType>
MatrixType& Matrix_base<MatrixType>::operator+=(MatrixType const &rhs)
{
auto it = std::begin(rhs.self()->values_);
std::for_each(begin(this->self()->values_), end(this->self()->values_), [&it](auto& el)
{
el += *it;
std::advance(it, 1);
});
return *(this->self());
}
// Implementations of free functions:
template <typename MatrixType>
typename std::enable_if< std::is_base_of <Matrix_base <MatrixType>, MatrixType>::value, MatrixType>::type
operator+(MatrixType lhs, MatrixType const &rhs)
{
return lhs += rhs;
}
template <typename MatrixType>
typename std::enable_if< std::is_base_of< Matrix_base< MatrixType >, MatrixType >::value, std::ostream>::type&
operator<<(std::ostream &os, MatrixType const& m)
{
std::copy(m.begin(), m.end(), // no ADL :(
std::ostream_iterator<typename MatrixType::value_type>{os, " "});
return os;
}
}
#endif
main.cpp:
#include <iostream>
#include "Matrix.hpp"
int
main ()
{
mbw::Dynamic_matrix<int> m{ 2, 2, { 1,1,1,1 } };
mbw::Dynamic_matrix<int> n{ 2, 2, { 2,2,2,2 } };
std::cout << m << '\n';
std::cout << n << '\n';
m += n;
std::cout << m << '\n';
std::cout << (m+n) << "\n\n";
mbw::Static_matrix<int, 2, 2> o { 3,3,3,3 };
mbw::Static_matrix<int, 2, 2> p {o};
std::cout << o << '\n';
std::cout << p << '\n';
o += p;
std::cout << o << '\n';
std::cout << (o+p) << '\n';
return 0;
}