I am looking at Bowie Owens' cppcon 2019 talk -- the slides can be found here. I came accross this talk as I was interested in introducing expression templates for a in-house numerical matrix-vector library that I am using, to improve its performance as well as the semantics of its client code.
I am not an expert in C++ and the talk's source code is not provided, so that I used the slides' example code snippets and came up with this :
//is_array_v
template <class T> struct is_array {
static constexpr bool value = false;
};
template <class T>
struct is_array<std::vector<T>> {
static constexpr bool value = true;
};
template <class T>
constexpr bool is_array_v =
is_array<std::remove_cvref_t<T>>::value;
struct expression {};
template <class callable, class... operands>
class expr : public expression
{
// I used reference semantics as I wish to avoid copying
// but pay attention not to use ref to a temp
// after the lifetime of the temp ended ...
// I use variadic template to tackle any arity operator at once
// and this mitigates the need for CRTP
std::tuple<operands const &...> args_;
callable f_;
public:
expr(callable f, operands const&... args)
: args_(args...), f_(f) {}
auto operator[](size_t const i) const
{
auto const call_at_index =
[this, i](operands const&... a)
{
return f_(subscript(a, i)...);
};
return std::apply(call_at_index, args_);
}
};
template <class T>
constexpr bool is_array_or_expression =
is_array_v<T> ||
std::is_base_of_v<expression, std::remove_cvref_t<T>>;
template <class A, class B>
constexpr bool is_binary_op_ok =
is_array_or_expression<A> ||
is_array_or_expression<B>;
//subscript()
template <class operand>
auto subscript(operand const& v, size_t const i) {
if constexpr (is_array_or_expression<operand>) {
return v[i];
}
else {
return v;
}
}
template <class LHS, class RHS>
auto operator+(LHS const & lhs, RHS const& rhs)
{
// I use a lambda so that everything is located in ONE place
return expr{
[](auto const& l, auto const& r)
{
return l + r; // deduced return types
}, lhs, rhs
};
}
class tridiagonal
{
std::vector<double> v_;
public:
tridiagonal(std::vector<double> v) : v_(std::move(v)) {}
template <class src_type>
tridiagonal& operator=(src_type const& src)
{
size_t const I = v_.size();
for (size_t i = 0; i < I; ++i) {
v_[i] = src[i];
}
return *this; // this line was missing in the slides and in the talk
}
};
Yet when I write client code :
tridiagonal a({ 1.0, 2.0, 3.0 });
tridiagonal b({ 0.5, 0.25, -4.5 });
tridiagonal c = a + b; // problematic line
at compilation the compiler tells me, on the problematic line, that :
Error C2440 'initializing': cannot convert from '+::<lambda_1>,LHS,RHS>' to 'tridiagonal'
with
[
LHS=tridiagonal,
RHS=tridiagonal
}
tridiagonal c = a + b
isn't an assignment, it's a construction. You need a constructor template that does something similar to your assignment template.
However your expr
class is currently missing size information, so the constructor can't initialise v_
to the right size.
Your tridiagonal
class is also missing operator[]
, which is needed to call expr::operator[]
, and it also lacks size
.
It is also problematic that you've not constrained your overload of operator+
. You define the trait is_binary_op_ok
but never use it.
With all these fixes, your example works
template <class operand>
auto size(operand const& v) {
if constexpr (is_array_or_expression<operand>) {
return v.size();
}
else {
return 1;
}
};
template <class callable, class... operands>
class expr : public expression
{
// I used reference semantics as I wish to avoid copying
// but pay attention not to use ref to a temp
// after the lifetime of the temp ended ...
// I use variadic template to tackle any arity operator at once
// and this mitigates the need for CRTP
std::tuple<operands const &...> args_;
callable f_;
public:
expr(callable f, operands const&... args)
: args_(args...), f_(f) {}
auto operator[](size_t const i) const
{
auto const call_at_index =
[this, i](operands const&... a)
{
return f_(subscript(a, i)...);
};
return std::apply(call_at_index, args_);
}
auto size() const
{
auto const call_size =
[](operands const&... a)
{
return std::max(::size(a)...);
};
return std::apply(call_size, args_);
}
};
template <class LHS, class RHS, class = std::enable_if_t<is_binary_op_ok<LHS, RHS>>>
auto operator+(LHS const & lhs, RHS const& rhs)
{
return expr{std::plus<>{}, lhs, rhs};
}
class tridiagonal : public expression
{
std::vector<double> v_;
public:
tridiagonal(std::vector<double> v) : v_(std::move(v)) {}
template <class src_type>
tridiagonal(src_type const& src) : v_(src.size())
{
size_t const I = v_.size();
for (size_t i = 0; i < I; ++i) {
v_[i] = src[i];
}
}
template <class src_type>
tridiagonal& operator=(src_type const& src)
{
size_t const I = v_.size();
for (size_t i = 0; i < I; ++i) {
v_[i] = src[i];
}
return *this; // this line was missing in the slides and in the talk
}
auto operator[](size_t const i) const
{
return v_[i];
}
auto size() const
{
return v_.size();
}
};
Having now watched the talk, this is intentional on the part of the author, they intend for these expressions to only apply to correctly sized arguments, and do no allocations themselves. In that case you only need to constrain operator +
and implement tridiagonal::operator[]
, and use an assignment rather than a construction.