I'm examining a Stroustroup's book "C++ Programming 4th edition". And I'm trying to follow his example on matrix design.
His matrix class heavily depends on templates and I try my best to figure them out. Here is one of the helper classes for this matrix
A Matrix_slice is the part of the Matrix implementation that maps a set of subscripts to the location of an element. It uses the idea of generalized slices (§40.5.6):
template<size_t N>
struct Matrix_slice {
Matrix_slice() = default; // an empty matrix: no elements
Matrix_slice(size_t s, initializer_list<size_t> exts); // extents
Matrix_slice(size_t s, initializer_list<size_t> exts, initializer_list<siz e_t> strs);// extents and strides
template<typename... Dims> // N extents
Matrix_slice(Dims... dims);
template<typename... Dims,
typename = Enable_if<All(Convertible<Dims,size_t>()...)>>
size_t operator()(Dims... dims) const; // calculate index from a set of subscripts
size_t size; // total number of elements
size_t start; // star ting offset
array<size_t,N> extents; // number of elements in each dimension
array<size_t,N> strides; // offsets between elements in each dimension
};
I
Here are the lines that build up the subject of my question:
template<typename... Dims,
typename = Enable_if<All(Convertible<Dims,size_t>()...)>>
size_t operator()(Dims... dims) const; // calculate index from a set of subscripts
earlier in the book he describes how Enable_if and All() are implemented:
template<bool B,typename T>
using Enable_if = typename std::enable_if<B, T>::type;
constexpr bool All(){
return true;
}
template<typename...Args>
constexpr bool All(bool b, Args... args)
{
return b && All(args...);
}
I have enough information to understand how they work already and by looking at his Enable_if implementation I can deduce Convertible function as well:
template<typename From,typename To>
bool Convertible(){
//I think that it looks like that, but I haven't found
//this one in the book, so I might be wrong
return std::is_convertible<From, To>::value;
}
So, I can undersand the building blocks of this template function declaration but I'm confused when trying to understand how they work altogather. I hope that you could help
template<typename... Dims,
//so here we accept the fact that we can have multiple arguments like (1,2,3,4)
typename = Enable_if<All(Convertible<Dims,size_t>()...)>>
//Evaluating and expanding from inside out my guess will be
//for example if Dims = 1,2,3,4,5
//Convertible<Dims,size_t>()... = Convertible<1,2,3,4,5,size_t>() =
//= Convertible<typeof(1),size_t>(),Convertible<typeof(2),size_t>(),Convertible<typeof(3),size_t>(),...
//= true,true,true,true,true
//All() is thus expanded to All(true,true,true,true,true)
//=true;
//Enable_if<true>
//here is point of confusion. Enable_if takes two tamplate arguments,
//Enable_if<bool B,typename T>
//but here it only takes bool
//typename = Enable_if(...) this one is also confusing
size_t operator()(Dims... dims) const; // calculate index from a set of subscripts
So what do we get in the end? This construct
template<typename ...Dims,typename = Enable_if<true>>
size_t operator()(Dims... dims) const;
The questions are:
Update: You can check the code in the same book that I'm referencing here The C++ Programming Language 4th edition at page 841 (Matrix Design)
This is basic SFINAE. You can read it up here, for example.
For the answers, I'm using std::enable_if_t
here instead of the EnableIf
given in the book, but the two are identical:
As mentioned in the answer by @GuyGreer, the second template parameter of is defaulted to void
.
The code can be read as a "normal" function template definition
template<typename ...Dims, typename some_unused_type = enable_if_t<true> >
size_t operator()(Dims... dims) const;
With the =
, the parameter some_unused_type
is defaulted to the type on the right-hand side. And as one does not use the type some_unused_type
explicitly, one also does not need to give it a name and simply leave it empty.
This is the usual approach in C++ also found for function parameters. Check for example operator++(int)
-- one does not write operator++(int i)
or something like that.
What's happening all together is SFINAE, which is an abbreviation for Substitution Failure Is Not An Error. There are two cases here:
First, if the boolean argument of std::enable_if_t
is false
, one gets
template<typename ...Dims, typename = /* not a type */>
size_t operator()(Dims ... dims) const;
As there is no valid type on the rhs of typename =
, type deduction fails. Due to SFINAE, however, it does not lead to a compile-time error but rather to a removal of the function from the overload set.
The result in practice is as if the function would have not been defined.
Second, if the boolean argument of std::enable_if_t
is true
, one gets
template<typename ...Dims, typename = void>
size_t operator()(Dims... dims) const;
Now typename = void
is a valid type definition and so there is no need to remove the function. It can thus be normally used.
Applied to your example,
template<typename... Dims,
typename = Enable_if<All(Convertible<Dims,size_t>()...)>>
size_t operator()(Dims... dims) const;
the above means that this function exists only if All(Convertible<Dims,size_t>()...
is true
. This basically means the function parameters should all be integer indices (me personally, I would write that in terms of std::is_integral<T>
however).
The missing constexpr
s notwithstanding, std::enable_if
is a template that takes two parameters, but the second one is defaulted to void
. It makes sense when writing up a quick alias to this to keep that convention.
Hence the alias should be defined as:
template <bool b, class T = void>
using Enable_if = typename std::enable_if<b, T>::type;
I have no insight into whether this default parameter is present in the book or not, just that this will fix that issue.
The assignment of a type is called a type alias and does what it says on the tin, when you refer to the alias, you're actually referring to what it aliases. In this case it means that when you write Enable_if<b>
the compiler handily expands that to typename std::enable_if<b, void>::type
for you, saving you all that extra typing.
What you get in the end is a function that is only callable if every parameter you passed to it is convertible to a std::size_t
. This allows overloads of functions to be ignored if specific conditions are not met which is more a powerful technique than just matching types up for selecting what function to call. The link for std::enable_if
has more information on why you would want to do that, but I warn beginners that this subject gets kinda heady.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With