Одинаковый шаблон для многих функций

0

Привет, ребята (и с новым годом!)

Я пишу (не совсем) простой проект в C++ (мой первый, исходящий из простого C). Мне было интересно, есть ли способ упростить определение для нескольких функций, имеющих один и тот же шаблон шаблона. Я думаю, что примером было бы лучше объяснить проблему.

Контекст

Предположим, что у меня есть класс "Set", который представляет собой список чисел, определенный как

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

Поэтому, если у меня есть экземпляр (например, Set<double>) и массив U array[N], где U - другой арифметический тип, а N - целое число, я хотел бы иметь возможность выполнять некоторые операции, такие как присвоение значений массив к массиву Set. Таким образом, я создаю шаблон функции внутри класса

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);
}

Проблема

Что касается моих тестов, то код выше работает отлично. Однако я считаю это ДЕЙСТВИТЕЛЬНО ДЕЙСТВИТЕЛЬНО уродливым, потому что мне нужно static_assert чтобы гарантировать, что в качестве аргументов принимаются только арифметические типы (параметр U), и мне нужен способ быть уверенным в размере массива (параметр N). Кроме того, я не выполнял функцию assign но мне нужно так много других функций, таких как add, multiply, scalar_product и т.д. scalar_product т.д.!

Первое решение

Тогда мне было интересно, есть ли более красивый способ написать этот класс. После некоторой работы я придумал директиву препроцессора:

#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__                                                                 \
        }

таким образом определяя мою функцию как

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

Это как-то чище, но все же не самое лучшее, так как я вынужден потерять скобки, обертывающие тело функции (нужно включить static_assert INSIDE, чтобы сама функция для параметра шаблона U была в области).

Вопрос

Решение, которое я нашел, похоже, работает очень хорошо, и код гораздо читабельнее, чем раньше, но... Не могу ли я использовать другую конструкцию, позволяющую мне создать static_assert определение всех функций и все еще сохраняя static_assert часть и информацию о размере массива? Было бы очень уродливо повторять код шаблона один раз для каждой функции, в которой я нуждаюсь...

Спасибо

Я просто пытаюсь узнать о языке, поэтому ЛЮБАЯ дополнительная информация об этом аргументе будет действительно оценена. Я искал столько, сколько мог, но ничего не мог найти (может быть, я просто не мог придумать подходящие ключевые слова, чтобы спросить Google, чтобы найти что-то релевантное). Заранее благодарю за вашу помощь, и счастливый новый год для вас всех !

Джанлука

  • 1
    Этот вопрос лучше подходит для CodeReview .
  • 0
    #define big no no: E
Показать ещё 9 комментариев
Теги:
templates
preprocessor-directive

2 ответа

5
Лучший ответ

Я настоятельно рекомендую против использования макросов, если между ними нет пути (случай, о котором я мог думать, получает номер строки для целей отладки). Из руководства по стилю Google C++ (http://google-styleguide.googlecode.com/svn/trunk/cppguide.xml#Preprocessor_Macros):

Макросы означают, что код, который вы видите, не совпадает с кодом, который видит компилятор. Это может привести к неожиданному поведению, тем более что макросы имеют глобальный охват.

Я действительно не понимаю, почему вы считаете использование static_assert уродливым. Существует еще один способ убедиться, что шаблон специализирован только для некоторых типов с использованием 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> {
};

и вы можете сделать это даже красивее, using инструкцию (не предназначенную для каламбура):

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>> {
};

И сейчас

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

SFINAE (Ошибка замены не является ошибкой) - это функция в C++, в которой вы не получаете ошибку, если специализация шаблона терпит неудачу.

template <bool Cond, class T = void> struct enable_if. Тип T включен как тип члена enable_if::type если Cond - true. В противном случае enable_if::type не определен. Таким образом, для типа float is_integral имеет значение false, а enable_if::type не существует, поэтому специализация шаблона

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

не выполняется, но вместо этого используется общий шаблон

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

который объявлен, но не определен.

Это полезно, поскольку вы можете иметь больше специальностей, таких как:

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>> {
};

Надеюсь, вы найдете это, по крайней мере, интересным.

С новым годом!

редактировать

Где я должен помещать <T, enable_if_integral_t<T>> в определение функции? Я могу сделать это только с помощью шаблонов классов...

Для функции тип enable_if :: может быть типом возврата. Например, если f возвращает int, вы можете:

#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;
}

и с 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;
}
  • 0
    Спасибо! Я попробовал этот подход, прежде чем перейти к тому, который я написал, но мне так и не удалось заставить его работать. Однако мой код был немного другим, поэтому я попробую еще раз и дам вам знать!
  • 0
    Попробовал, но не смог заставить его работать! Куда мне поместить <T, enable_if_integral_t<T>> в определении функции ? Я могу сделать это только с помощью шаблонов классов ...
Показать ещё 2 комментария
1

Я не понимаю, почему вы считаете утверждения static_assert или errlog настолько уродливыми и подозреваете, что они частично не знакомы с языком. Тем не менее, вы можете легко написать функцию или макрос (если вы хотите использовать __LINE__ внутри функции assign и т.д.), Чтобы вывести их из строя, что позволяет использовать:

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);
}

Не могу ли я использовать другую конструкцию, позволяющую мне создать четкое определение всех функций и все еще сохраняя часть static_assert и информацию о размере массива? Было бы очень уродливо повторять код шаблона один раз для каждой функции, в которой я нуждаюсь...

Что касается возможного - хотя ИМХО, вероятно, нежелательного обфускации - вы могли бы иметь ваши функции, принимающие (а) аргументы, у которых есть шаблонный неявный конструктор из массива, проверяя его арифметику в этом конструкторе, а затем проверяя размер в используемой функции, позволяя использовать:

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);
}

Реализация:

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_;
};

обсуждение

"Чище" может быть субъективным. В частности, вы должны рассмотреть значение "нормального" источника немакросообщений с использованием интуитивного типа C++ в качестве документации и для удобства обслуживания. Если макрос существенно упрощает многие функции, и особенно если он используется только в файле реализации, а не в общем заголовке, тогда это стоит того, но если есть только предельное преимущество, то это не стоит зафускацию и де-локализацию. Весь этот материал шаблона может казаться запутанным и уродливым, когда вы новичок в языке, но через некоторое время он понял с первого взгляда и помогает читателям понять, что функция продолжает делать.

В C++ также широко распространено отношение "утиная печать" к параметрическому полиморфизму шаблонов. Это означает, что вы можете позволить людям передавать аргументы любого типа, и если эти типы поддерживают операции, которые пытается реализовать им реализация шаблона (т.е. компилируют), то, надеюсь, это будет то, что хочет вызывающий. Это одна из причин, по которой хорошей идеей создавать типы, которые предсказуемы для семантического поведения, например, - только с использованием перегрузки оператора, когда аффект похож на те же самые операторы на встроенных типах или std::string.

Более жесткое правоприменение, на которое вы хотели бы иметь свое место, - Bjarne Stroustrup и другие, потратили много времени на разработку "Концепций", которые являются механизмом для обеспечения ожиданий по типам, используемым в качестве параметров шаблона, и были бы хороши для вашего "арифметические типы". Надеюсь, они перейдут в следующий стандарт C++. Между тем, статические утверждения - хороший способ пойти.

  • 0
    Спасибо за ответ! Однако весь смысл создания такой функции заключается в том, чтобы иметь возможность назначать стандартный массив C для моего внутреннего массива Set поэтому создание другого класса-оболочки не будет работать ... Мне нужно иметь возможность сделать что-то вроде Set<double> s1(3); затем s1 = (double[3]) {1, 2, 3}; Это возможно с вашим подходом?
  • 0
    but after a while it's understood at a glance and helps readers understand what the function goes on to do. Понял. Я удаляю макрос и помещаю все в строку, ваше утверждение имеет большой смысл.

Ещё вопросы

Сообщество Overcoder
Наверх
Меню