11

I was wondering if anyone knows a way of creating repetitive type declarations, and since this might be confusing an example will be helpful:

For our project we need to have function and type declarations like:

    using FunType = std::function<void(double,double,double,double,double,double,double,double)>;
    using DataType = std::tuple<double,double,double,double,double,double,double,double>;

scattered around several files, multiple times, up to 32 doubles (only doubles, if this is important).

I would be happy to replace the manual writing/counting of those doubles, with something in the lines of the following imaginary code:

    using FunType = something_generate_function_decl<8>;
    using DataType = something_generate_datatype_decl<8>

If possible I would like to stay away from boost and its preprocessor library.

Edit to provide some clarification

The big picture is that at some point in the application we get in a series of bytes (representing an array of doubles) with each value having a predefined meaning, we need to do validation on them (each value needs to be verified with several conditions for validity), reject data that is not valid, log the corresponding invalid data with its meaning, pass the meaningful data around the application, from logger module to a dozen of different places including existing functions and also Qt signals with already defined syntax etc... and at some point this will go out from our hands, so that's why I would like to create code as easily readable and verifiable as possible.

Edit2 to provide more clarification

Reading the comments, seemingly there is a lot of confusion about the scope of this question. There is intentionally a lot of information missing from the question which is not relevant to the pure essence of the question which is how to shorten those 8 (or 9, or 12, or 32 doubles) into a more manageable entity. The missing information is our internal way of dealing with the data we receive (we have a project of a large scale, so you can imagine that there are a few layers of abstraction for the data which does automatic validation, conversion, etc... before sending over, so not the entire application is a long list of if/else statements and basic function calls with parameters), the only restriction for the entire thing is that what comes in as a simple array of doubles (representing a message) is validated and then sent over to functions/QT signals/C++11 lambdas which already have a predefined interface. All the information handling and validation is encapsulated into layers of classes which just register themselves somewhere (out of scope for this question) for receiving, storing, validating and sending data, and for example, the FunType (which actually is an internally used type of the message classes) represents the interface of the functions/slots/lambdas where the specific message (with its validated members) will be sent, automatically by some mechanism which gathers all the members into a tuple (DataType) and the using some index_sequence magic with variadic templates the compiler matches the tuple to the required function which at some point in time "subscribed" to this message. @admin if you feel this edit not being relevant, feel free to delete it.

Ferenc Deak
  • 34,348
  • 17
  • 99
  • 167
  • 1
    Wouldn't be better to use an _array_ of doubles? – Daniel Langr Jul 30 '20 at 07:45
  • @DanielLangr we tried that too, but the code maintainability was down to zero at some point so we decided to make some human readable functions – Ferenc Deak Jul 30 '20 at 07:48
  • 2
    How is `std::array` _less_ maintainable than `double, double, double, double, double, double, double, double` ? – Useless Jul 30 '20 at 07:59
  • @Useless consider this (not real code, just example): `void fun(std::array a) { if(a[0] > a[1] && a[1] < 90 && a[1] > 0 && a[3] > 0) fly(a[0], a[1], a[2]) }` vs. `void fun(double latitude, double longitude, double altitude) { if(latitude > longitude && longitude < 90 && longitude > 9 and altitude > 0) fly(longitude, latitude, altitude); }` Which one do you understand more what it does? – Ferenc Deak Jul 30 '20 at 08:08
  • 6
    So the information missing from your question is that the 8 (or 32) doubles are not a flat unstructured list, but are in fact named variables. In which case you should be using a struct with named members, or a struct with a tuple and named _accessors_, instead. – Useless Jul 30 '20 at 08:12
  • @Useless please see my latest edit to the question itself to clarify some things – Ferenc Deak Jul 30 '20 at 12:12

5 Answers5

9

This is easy with Boost.Mp11:

#include <boost/mp11/algorithm.hpp>
#include <boost/mp11/list.hpp>

namespace mp11 = boost::mp11;

template<class... Args>
using make_fn = std::function<void(Args...)>;

using TypeList = mp11::mp_repeat_c<mp11::mp_list<double>, 8>;
using FunType  = mp11::mp_apply<make_fn,    TypeList>;
using DataType = mp11::mp_apply<std::tuple, TypeList>;

An alternative C++14 solution without Boost:

template<template<class...> class Fn, class T, std::size_t n>
struct apply_repeat {
    template<std::size_t... is>
    static Fn<decltype(is, T{})...> 
        get_type(std::index_sequence<is...>);

    using type = decltype(get_type(std::make_index_sequence<n>{}));
};

template<class... Args>
using make_fn = std::function<void(Args...)>;

using FunType  = typename apply_repeat<make_fn,    double, 8>::type;
using DataType = typename apply_repeat<std::tuple, double, 8>::type;

This solution requires a default constructible T. This requirement is satisfied for double.

To lift this requirement we can use type_identity wrapper (will be part of C++20):

template<class T> 
struct type_identity {
    using type = T;
};

template<template<class...> class Fn, class T, std::size_t n>
struct apply_repeat {
    template<std::size_t... is>
    static Fn<typename decltype(is, type_identity<T>{})::type...> 
        get_type(std::index_sequence<is...>);

    using type = decltype(get_type(std::make_index_sequence<n>{}));
};
Evg
  • 25,259
  • 5
  • 41
  • 83
  • Neat, I also did try to use return types but I did not manage to get it working. I think you can use `std::declval` to allow any `T`s: `static Fn())>...> `. It does not work for references though, that could be achieved with replacing `remove_ref_t` with [`copy_cv_ref_t`](https://stackoverflow.com/questions/31171682/type-trait-for-copying-cv-reference-qualifiers) from T itself. – Quimby Jul 30 '20 at 09:22
  • 1
    @Quimby, thanks for a suggestion. I deliberately used `T{}` to make code simple (OP states that only `double` has to be supported). Instead of `std::declval` I could suggest using the `type_identity` wrapper. Added it into an answer. – Evg Jul 30 '20 at 09:38
  • That is even better, I did not know about it and it even has a simple implementation if C++20 is not supported yet. – Quimby Jul 30 '20 at 09:42
4

No need to drag Boost into this, here is C++11 solution:

#include <cstdint>

template<std::size_t N,typename R,typename T, typename...Args>
struct function_replicator{
    //Add an extra T that will be packed with Args... in the nested template.
    using type = typename function_replicator<N-1,R,T,T,Args...>::type;
};
template<typename R,typename T, typename...Args>
struct function_replicator<0,R,T,Args...>{
    //Args... holds N Ts
    using type = R(Args...);
};

template<std::size_t N,template<typename...CArgs>class Container,typename T, typename...Args>
struct container_replicator{
    using type = typename container_replicator<N-1,Container,T,T,Args...>::type;
};
template<template<typename...CArgs>class Container,typename T, typename...Args>
struct container_replicator<0,Container,T,Args...>{
    using type = Container<Args...>;
};

#include <tuple>
#include <functional>

// Feel free to make them more general.
template<std::size_t N>
using function_def = std::function<typename function_replicator<N,void,double>::type>;

template<std::size_t N>
using tuple_def = typename container_replicator<N,std::tuple,double>::type;


#include <type_traits>

int main(){
    //Keeping it C++11
    static_assert(std::is_same<function_def<3>,std::function<void(double,double,double)>>::value,"");
    static_assert(std::is_same<tuple_def<3>,std::tuple<double,double, double>>::value,"");
}
Quimby
  • 17,735
  • 4
  • 35
  • 55
  • 1
    Most excellent! Even though I personally think that such types are a terrible design decision. – Waqar Jul 30 '20 at 08:57
  • 3
    Thank you, I agree but OP seems experienced enough and the question is quite clear. Plus it was a fun metaprogramming exercise too. – Quimby Jul 30 '20 at 09:05
3

Here is pure C++ solution without Boost or Macros, just variadic templates:

#include <tuple>
#include <utility>
#include <functional>

// tuple
template <typename T, std::size_t ... Is>
constexpr auto gft_helper (std::index_sequence<Is...> const &)
-> decltype(std::make_tuple( (Is, std::declval<T>())... ));

template <typename T, std::size_t N>
constexpr auto get_fixed_tuple ()
-> decltype(gft_helper<T>(std::make_index_sequence<N>{}));

template <typename T, std::size_t N>
using tuple_fixed_type = decltype(get_fixed_tuple<T, N>());

// function
template <typename T>
constexpr auto getType(int, T&& t)
-> typename std::decay<T>::type;

template <typename T, std::size_t ... Is>
constexpr auto gff_helper (std::index_sequence<Is...> const &)
-> std::function<void( decltype(getType(Is, std::declval<T>()))...)>;

template <typename T, std::size_t N>
constexpr auto get_fixed_function ()
-> decltype(gff_helper<T>(std::make_index_sequence<N>{}));

template <typename T, std::size_t N>
using function_fixed_type = decltype(get_fixed_function<T, N>());

int main()
{
    using FunType = function_fixed_type<double,4>;
    using DataType = tuple_fixed_type<double,4>;

    static_assert(std::is_same<DataType, std::tuple<double, double, double, double>>{} );
    static_assert(std::is_same<FunType, std::function<void(double, double, double, double)>>{} );
 }
Vasilij
  • 1,861
  • 1
  • 5
  • 9
2

If you can use Boost, there's a macro BOOST_PP_ENUM in it's Preprocessor library that can help. This should work:

#include <boost/preprocessor/repetition/enum.hpp>

#define TEXT(z, n, data) data

using FunType = std::function<void( BOOST_PP_ENUM(8, TEXT, double) )>;
using DataType = std::tuple< BOOST_PP_ENUM(8, TEXT, double) >;

Live demo: https://godbolt.org/z/537n74

Or, more comfortably with additional helper macro:

#define NDOUBLE(n) BOOST_PP_ENUM(n, TEXT, double)

using FunType = std::function<void( NDOUBLE(8) )>;
using DataType = std::tuple< NDOUBLE(8) >;

Live demo: https://godbolt.org/z/vq1jrY

Daniel Langr
  • 22,196
  • 3
  • 50
  • 93
2

tl;dr - this is a frame challenge because I think you're asking the wrong question.

For our project we need to have function and type declarations like:

 using FunType = std::function<void(double,double,double,double,double,double,double,double)>;
 using DataType = std::tuple<double,double,double,double,double,double,double,double>;

Well, don't. If you love pointless repetition so much, you should be doing your calculations by hand instead of using a computer.

I would be happy to replace the manual writing/counting of those doubles, with something in the lines of the following imaginary code:

using FunType = something_generate_function_decl<8>;

Your aspirations are too low. You're just automating the production of bad code, when you could be writing good code instead.

Your final, grudgingly-revealed actual example of what you want to handle:

void fun(double latitude, double longitude, double altitude)
{
  if(latitude > longitude &&
     longitude < 90 && longitude > 9 &&
     altitude > 0)
  {
    fly(longitude, latitude, altitude);
  }
}

(once I removed the tabs and the mixed use of && and and which did nothing to reassure me about the quality of your existing code) ... would be much better handled by using structured types with named fields than any of the things you asked for.

struct LatLongPosition // just in case you have alternative representations
{
    double latitude;
    double longitude;
};

struct AirPosition
{
    LatLongPosition pos;
    double altitude;
};

Now, your function arguments can be strongly-typed: rather than accepting any sequence of 8 or 32 doubles, you can guarantee at compile-time you're passing the correct type of position (if you have more than one), and a position rather than a velocity or momentum or anything else.

Note that there's lots of scope to improve further, but the foundation is to be using structured data types rather than massive flat argument lists in the first place.

Useless
  • 64,155
  • 6
  • 88
  • 132
  • While I can see the valid points in your arguments, our application is just the message passing part of the entire infrastructure, so we do not need to create structures, because we are not handling the data. We just need to make sure that what arrives as a raw array of doubles is valid and that it is sent forward to functions which are already written (some of them are C functions, some of them are Qt signals, again with a very well defined interface). This short sequence of code from the question is just the part of an "automated" decompress, validate, forward mechanism from the app. – Ferenc Deak Jul 30 '20 at 08:41
  • 1
    Validation _is_ handling the data. You need to know what the data represents to validate it, and flat argument lists are a terrible way to encode this information. – Useless Jul 30 '20 at 08:44