Stan Math library C++ code modernization ideas

@cqfd was messaging me about some ideas to revamp a lot of our template metaprograms (these usually live in the various meta directories in the Math library) using the C++11 features (and some C++14 features) we have access to now, so I wanted to start a public thread on these ideas. Some people have been finding a few interesting use cases for variadic templates, though I think there are a lot that haven’t been touched yet. What are some other ideas? Here are a couple I’m aware of:

  • @cqfd was talking about replacing our template <> struct { value } patterns that you can see in many of our metaprograms with template variables, but those only become available in gcc 5 and we’re stuck on 4.9.3 until RTools migrates to a higher version next year. But I was thinking we might still be able to somehow use (possibly templated) constexpr functions to do something better now for at least the value structs.
  • @cqfd was also talking about switching to using for type aliases, which I think would be a good idea and might clarify the code a bit (and it’s recommended now).

@Bob_Carpenter @sakrejda @Matthijs @anon75146577 any ideas you’ve been pondering and want to bring up here?

Played around with templated constexpr functions and you can mimic our TMPs like is_constant_struct like this:


template <typename>
struct type {};

template <typename T>
constexpr bool is_constant(type<T>) { return boost::is_convertible<T, double>::value; }

template <typename T>
constexpr bool is_constant_struct(type<T>) { return is_constant<T>(type<T>{}); }

template <typename T>
constexpr bool is_constant_struct(type<std::vector<T>>) { return is_constant<T>
    (type<T>{}); }
... <more specializations would follow>

Usage is roughly as pretty/gross as the current structs,

is_constant_struct(type<T_shape>{})

vs the current,

is_constant_struct<T_shape>::value

Oh well. The idea for using overloading instead of partial template specialiation (which functions in C++ famously don’t support) came from this blog post.

1 Like

As a little example of what variable templates look like (for whenever Stan gets on gcc 5, that is): https://gist.github.com/cqfd/ec95134ddbded22bea8edfb85afc9866

They’re admittedly not quite as flexible as the struct::value approach (they’re not structs, so you can’t inherit from them, and I don’t think you can use them as template arguments?), but they’re a little less noisy and I think they’re less annoying to write.

Another common thing to do (e.g. in the standard library) is to use the struct::value approach, but then give shortcuts like

template <typename T>
constexpr bool is_constant_v = is_constant<T>::value;

Or, for metafunctions that return types:

template <typename T>
using is_vector_t = typename is_vector<T>::type;

Not a huge simplification or anything, but it’s a little less noisy and it’s what the standard library does.

2 Likes

I believe math/stan/math/prim/scal/meta/contains_nonconstant_struct.hpp and math/stan/math/prim/scal/meta/contains_vector.hpp both are possible candidates for modernization. Take contains_vector as an example. We could use the (not quite) recursion brought by variadic tempaltes to replace the following code.

template <typename T1, typename T2 = double, typename T3 = double,
          typename T4 = double, typename T5 = double, typename T6 = double>
struct contains_vector {
  enum {
    value = is_vector<T1>::value || is_vector<T2>::value || is_vector<T3>::value
            || is_vector<T4>::value || is_vector<T5>::value
            || is_vector<T6>::value
  };
};

The new code would have a base case

template<typename T>
struct contains_vector{
    enum {
        value = is_vector<T>::value
    };
};

and a recursive case

template<typename T, typename... ts>
struct contains_vector{
    enum {
        value = (is_vector<T>::value || contains_vector<ts...>::value)
    };
};

This idea loses the double default types. Is this OK? My reading suggests that those are there as a default to false (not vector), which wouldn’t be necessary under the more modern pattern.

2 Likes

Yeah, definitely @roualdes, I was thinking along the same lines. (My gist above has a variable-template version of contains_nonconstant_struct.) I also wasn’t quite sure if it was ok to get rid of the double defaults, but seemed like it was.

You can keep the double default although in this case it doesn’t do anything (it just evaluates to false) by putting a default on the most general template (which is then inherited by its specialisations. It’s an error to have a default on a partial specialisation). See include_summand here https://github.com/stan-dev/math/pull/978

1 Like

It’s quite possible that default didn’t make it into the final code, but Daniel and I talk about it at one point in the thread iirc (on my phone so can’t check. Sorry)

1 Like

I posted some issues on things we want to do. One of the bigger ones is using auto where possible to preserve Eigen expression templates rather than evaluating them.

We want to use more of the algorithm libraries, standard copy, etc. A lot of things we can do will be simplified with bind-free lambdas. We should be using foreach loops. We should be using delegating constructors. Lots of stuff!

2 Likes

@cqfd Super clean PR! Would you mind double checking my reading of your code? Two questions.

  1. Will the following code only match if no std::vector has been found and 0 template arguments remain?
template <typename... Ts>
struct contains_std_vector : std::false_type {};
  1. Will your design short circuit in the sense that it stops looking for a std::vector as soon as one is found?

Edit: accidentally posted before I finished my thought – darn extra wide touchpad.

I’m not @cqfd but my timezone is closer - yes and yes. I like this blog post on these: https://www.fluentcpp.com/2017/06/02/write-template-metaprogramming-expressively/

@roualdes, yeah, exactly. About your first question, there is a slightly more explicit way to write the recursion that makes the empty base case more obvious:

template <typename... Ts> struct contains_std_vector; // declaration only

template <>
struct contains_std_vector<> : false_type {};

template <typename T, typename... Ts>
struct contains_std_vector<vector<T>, Ts...> : true_type {};

template <typename T, typename Ts...>
struct contains_std_vector<T, Ts...> : contains_std_vector<Ts...> {};

That ends up being equivalent to the code in the PR, just slightly shorter. The parent template in the PR says “I apply to any number of types”, and then specialization only succeeds if the types match those more specific templates (which only apply to non-empty parameter packs, and the most specific one to a parameter pack starting with std::vector).

I don’t really have a strong preference… I suppose the more explicit base case is easier to follow. I mention in the PR that if you need to do this kind of recursion a lot, you might want to have a higher-order template that checks if anything in a parameter pack matches a predicate; then you only have to get the recursion right once.

But about template modernization tricks, one thing I like about the style above is that you can express the recursion very succinctly with inheritance. (The standard library structs std::true_type and std::false_type just have a single boolean value field.) Walter Brown suggests doing something similar for type fields:

template <typename T> struct type_is { using type = T; };

// e.g. metafunction to remove cv qualifiers from a type
template <typename T> struct remove_cv : type_is<T> {};
template <typename T> struct remove_cv<T const> : type_is<T> {};
template <typename T> struct remove_cv<T volatile> : type_is<T> {};

Another maybe-not-huge simplification, but it’s cute :)

Walter Brown’s talks are a great intro to some fancier stuff like SFINAE, void_t, enable_if_t, etc.: https://www.youtube.com/watch?v=Am2is2QCvxY

1 Like

Thanks, @seantalts and @cqfd. I appreciate the links.