Я пишу (не совсем) простой проект в 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, чтобы найти что-то релевантное). Заранее благодарю за вашу помощь, и счастливый новый год для вас всех !
Джанлука
Я настоятельно рекомендую против использования макросов, если между ними нет пути (случай, о котором я мог думать, получает номер строки для целей отладки). Из руководства по стилю 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;
}
<T, enable_if_integral_t<T>>
в определении функции ? Я могу сделать это только с помощью шаблонов классов ...
Я не понимаю, почему вы считаете утверждения 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++. Между тем, статические утверждения - хороший способ пойти.
Set
поэтому создание другого класса-оболочки не будет работать ... Мне нужно иметь возможность сделать что-то вроде Set<double> s1(3);
затем s1 = (double[3]) {1, 2, 3};
Это возможно с вашим подходом?
but after a while it's understood at a glance and helps readers understand what the function goes on to do.
Понял. Я удаляю макрос и помещаю все в строку, ваше утверждение имеет большой смысл.
#define
big no no: E