Identical template for many functions

烈酒焚心 提交于 2020-01-14 06:13:10

问题


Hello guys (and happy new year!)

I'm writing a (not really) simple project in C++ (my first, coming from plain C). I was wondering if there is a way to simplify the definition for multiple functions having the same template pattern. I think an example would be better to explain the problem.

The context

Let's assume I have a "Set" class which represents a list of numbers, defined as

template <class T>
class Set {
    static_assert(std::is_arithmetic<T>(), "Template argument must be an arithmetic type.");
    T *_address;
    ...
}

So if I have an instance (say Set<double>) and an array U array[N], where U is another arithmetic type and N is an integer, I would like to be able to perform some operations such as assigning the values of the array to those of the Set. Thus I create the function template inside the class

template <class U, int N>
void assign(U (&s)[N]) {
    static_assert(std::is_arithmetic<U>(), "Template argument must be an arithmetic type.");
    errlog(E_BAD_ARRAY_SIZE, N == _size);
    idx_t i = 0;
    do {
        _address[i] = value[i];
    } while (++i < size);
}

The problem

As far as my tests went the code above works perfectly fine. However I find it REALLY REALLY ugly to see since I need the static_assert to ensure that only arithmetic types are taken as arguments (parameter U) and I need a way to be sure about the array size (parameter N). Also, I'm not done with the assign function but I need so many other functions such as add, multiply, scalar_product etc. etc.!

The first solution

I was wondering then if there is a prettier way to write this kind of class. After some work I've come up with a preprocessor directive:

#define arithmetic_v(_U_, _N_, _DECL_, ...)                                             \
        template <class U, idx_t N> _DECL_                                              \  
        {                                                                               \
            static_assert(std::is_arithmetic<U>(),"Rvalue is not an arithmetic type."); \
            errlog(E_BAD_ARRAY_SIZE, N == _size);                                       \
            __VA_ARGS__                                                                 \
        }

thus defining my function as

arithmetic_v(U, N,
             void assign(U (&value)[N]),
                 idx_t i = 0;
                 do {
                     _address[i] = value[i];
                 } while (++i < _size);
             )

This is somehow cleaner but still isn't the best since I'm forced to lose the brackets wrapping the function's body (having to include the static_assert INSIDE the function itself for the template parameter U to be in scope).

The question

The solution I've found seems to work pretty well and the code is much more readable than before, but... Can't I use another construct allowing me to build an even cleaner definition of all the functions and still preserving the static_assert piece and the info about the array size? It would be really ugly to repeat the template code once for each function I need...

The thanks

I'm just trying to learn about the language, thus ANY additional information about this argument will be really appreciated. I've searched as much as I could stand but couldn't find anything (maybe I just couldn't think of the appropriate keywords to ask Google in order to find something relevant). Thanks in advance for your help, and happy new year to you all!

Gianluca


回答1:


I strongly suggest against using macros, unless there is no way around them (a case I could think of is getting the line number for debugging purposes). From the Google C++ Style Guide (http://google-styleguide.googlecode.com/svn/trunk/cppguide.xml#Preprocessor_Macros):

Macros mean that the code you see is not the same as the code the compiler sees. This can introduce unexpected behavior, especially since macros have global scope.

I really don't see why you consider using a static_assert ugly. There is another way to ensure that a template is specialized only for some types using SFINAE.

template <class T, class Enable = void>
class X;

template <class T>
class X<T, typename std::enable_if<std::is_integral<T>::value>::type> {
};

and you could do this even prettier using a using statement (no pun intended):

template <class T>
using enable_if_integral_t = typename std::enable_if<std::is_integral<T>::value>::type;

template <class T, class Enable = void>
class X;

template <class T>
class X<T, enable_if_integral_t<T>> {
};

And now

X<int> x; // ok, int is integral
X<float> y; // compile error

SFINAE (Substitution failure is not an error) is a feature in C++ in which you don't get an error if a template specialization fails.

template <bool Cond, class T = void> struct enable_if. The type T is enabled as member type enable_if::type if Cond is true. Otherwise, enable_if::type is not defined. So for a float type is_integral is false and enable_if::type doesn't exist, so the template specialization

template <class T>
class X<T, typename std::enable_if<std::is_integral<T>::value>::type>

fails, but the generic template is used instead

template <class T, class Enable = void>
class X;

which is declared, but not defined.

This is useful as you can have more specializations like:

template <class T>
using enable_if_integral_t = typename std::enable_if<std::is_integral<T>::value>::type;

template <class T>
using enable_if_floating_t = typename std::enable_if<std::is_floating_point<T>::value>::type;


template <class T, class Enable = void>
class X;

template <class T>
class X<T, enable_if_integral_t<T>> {
};
template <class T>
class X<T, enable_if_floating_t<T>> {
};

Hope you find this at least interesting.

Happy new year!

Edit

Where should I put the <T, enable_if_integral_t<T>> in a function definition? I can only get this done with class templates...

For a function, the enable_if::type can be the return type. For example if f returns int, you can have:

#include <type_traits>

template <class T>
typename std::enable_if<std::is_integral<T>::value, int>::type f(T a) {
    return 2 * a;
}

int main() {
    f(3); // OK
    f(3.4); // error
    return 0;
}

and with using:

#include <type_traits>

template <class T, class Return = void>
using enable_if_integral_t = typename std::enable_if<std::is_integral<T>::value, Return>::type;

template <class T>
enable_if_integral_t<T, int> f(T a) {
    return 2 * a;
}

int main() {

    f(3); // OK
    f(3.4); // Error
    return 0;
}



回答2:


I don't understand why you consider the static_assert or errlog statements so ugly, and suspect it's partly unfamiliarity with the language. Still, you could easily write a function or macro (if you want to use __LINE__ inside the assign etc. function), to move them out of line, allowing usage like:

template <class U, int N>
void assign(U (&s)[N]) {
    assert_array_n_numbers(s);
    idx_t i = 0;
    do {
        _address[i] = s[i];
    } while (++i < size);
}

Can't I use another construct allowing me to build an even cleaner definition of all the functions and still preserving the static_assert piece and the info about the array size? It would be really ugly to repeat the template code once for each function I need...

In terms of what's possible - though IMHO likely undesirable obfuscation - you could have your functions accept (an) argument(s) that has a templated implicit constructor from an array, verifying it's arithmetic in that constructor then verifying size in the function using it, allowing usage like:

template <typename U>
void assign(Arithmetic_Array<U>& s) {
    assert_same_size(s);
    idx_t i = 0;
    do {
        _address[i] = s[i];
    } while (++i < size);
}

Implementation:

template <typename T>
class Arithmetic_Array
{
  public:
    template <size_t N>
    Arithmetic_Array(T (&a)[N])
      : p_(&a), size_(N)
    {
        static_assert(std::is_arithmetic<T>(),"Rvalue is not an arithmetic type.");
    }

    T& operator[](size_t i) { return p_[i]; }
    const T& operator[](size_t i) const { return p_[i]; }

    size_t size() const { return size_; }

  private:
    T* p_;
    size_t size_;
};

Discussion

"cleaner" can be subjective. In particular, you should consider the value of "normal" non-macro using-the-intuitive-type C++ source as documentation and for maintainability. If a macro substantially simplifies many functions - and particularly if it's only used in an implementation file and not a shared header - then it's worthwhile, but if there's only marginal benefit it's not worth the obfuscation and de-localisation. All that template stuff might seem convoluted and ugly when you're new to the language, but after a while it's understood at a glance and helps readers understand what the function goes on to do.

It's also common in C++ to embrace a "duck typing" attitude to template's parametric polymorphism. That means that you can let people pass in arguments of whatever type, and if those types support the operations that the template implementation attempts on them (i.e. compile), then hopefully it'll be what the caller wants. That's one reason that it's a good idea to create types that have predictable semantic behaviour, for example - only using operator overloading when the affect is similar to the same operators on built in types or std::string.

The stricter enforcement you'd like has its place though - Bjarne Stroustrup and others have spent a lot of time working on "Concepts" which are a mechanism for enforcing expectations on types used as template parameters, and would have been a good fit for your "arithmetic types" stipulation here. I hope they'll make it into the next C++ Standard. Meanwhile, static assertions are a good way to go.



来源:https://stackoverflow.com/questions/20895184/identical-template-for-many-functions

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!