Довольно простой шаблон 2-мерного вектора с операторами и двумя служебными функциями, использующими концепции C ++ 20. Шаблоны только для заголовков, встраивание функций, перегрузка операторов и т. Д. — не совсем мои сильные стороны, поэтому я написал это в надежде получить обратную связь.
#pragma once
#include <cmath>
#include <type_traits>
namespace math {
template<typename T>
concept Arithmetic = std::is_arithmetic_v<T>;
template<Arithmetic T>
struct Vector2D
{
T X = 0;
T Y = 0;
Vector2D() = default;
Vector2D(T x, T y);
Vector2D(const Vector2D<T> &other);
inline T Magnitude() const;
inline Vector2D<T> Normal() const;
};
template<Arithmetic T>
inline Vector2D<T>::Vector2D(T x, T y) : X(x), Y(y)
{}
template<Arithmetic T>
inline Vector2D<T>::Vector2D(const Vector2D<T> &other) : X(other.X), Y(other.Y)
{}
template<Arithmetic T>
inline T Vector2D<T>::Magnitude() const
{
return std::sqrt(X * X + Y * Y);
}
template<Arithmetic T>
inline Vector2D<T> Vector2D<T>::Normal() const
{
auto magnitude = Magnitude();
return Vector2D<T>(X / magnitude, Y / magnitude);
}
template<Arithmetic T>
inline Vector2D<T> operator-(const Vector2D<T> &vec)
{
return Vector2D<T>(-vec.X, -vec.Y);
}
template<Arithmetic T>
inline Vector2D<T> operator+(const Vector2D<T> &first, const Vector2D<T> &second)
{
return Vector2D<T>(first.X + second.X, first.Y + second.Y);
}
template<Arithmetic T>
inline Vector2D<T> operator-(const Vector2D<T> &first, const Vector2D<T> &second)
{
return Vector2D<T>(first.X + second.X, first.Y + second.Y);
}
template<Arithmetic T>
inline Vector2D<T> operator*(const Vector2D<T> &vec, const T factor)
{
return Vector2D<T>(vec.X * factor, vec.Y * factor);
}
template<Arithmetic T>
inline Vector2D<T> operator*(const T factor, const Vector2D<T> &vec)
{
return Vector2D<T>(factor * vec.X, factor * vec.Y);
}
template<Arithmetic T>
inline Vector2D<T> operator/(const Vector2D<T> &vec, const T divisor)
{
return Vector2D<T>(vec.X / divisor, vec.Y / divisor);
}
template<Arithmetic T>
inline Vector2D<T> &operator+=(Vector2D<T> &first, const Vector2D<T> &second)
{
first.X += second.X;
first.Y += second.Y;
return first;
}
template<Arithmetic T>
inline Vector2D<T> &operator-=(Vector2D<T> &first, const Vector2D<T> &second)
{
first.X -= second.X;
first.Y -= second.Y;
return first;
}
template<Arithmetic T>
inline Vector2D<T> &operator*=(Vector2D<T> &vec, const T factor)
{
vec.X *= factor;
vec.Y *= factor;
return vec;
}
template<Arithmetic T>
inline Vector2D<T> &operator/=(Vector2D<T> &vec, const T factor)
{
vec.X /= factor;
vec.Y /= factor;
return vec;
}
template<Arithmetic T>
inline Vector2D<T> &operator==(const Vector2D<T> &first, const Vector2D<T> &second)
{
return (first.X == second.X) && (second.Y == second.Y);
}
template<Arithmetic T>
inline Vector2D<T> &operator!=(const Vector2D<T> &first, const Vector2D<T> &second)
{
return !(first == second);
}
}
3 ответа
Обзор
Ваш код в том виде, в котором он написан, прекрасен (за исключением одной ошибки вычитания). Все, что здесь упоминается, в основном предназначено для помощи будущим читателям.
Вы чрезмерно усложнили дизайн, добавив конструкторы в Vector2D
.
Конструкторы по умолчанию работают отлично и, как и ожидалось, в подобной ситуации.
Есть аргумент, чтобы сделать операторов членами класса, а не отдельными функциями. Но либо работать.
Ваше использование справочного маркера &
очень нравится. Вы помещаете его рядом с переменной, а не рядом с типом. Это очень субъективный стиль (обычно продиктованный вашим руководством по стилю). Но руководства C ++ искажают одну сторону, а руководства C — другую.
void doStuff(C const& param)
^^^^^^^^ param has a type: C const&
void doStuff(C& param)
^^ param has a type: C&
Это произошло потому, что в C ++ типы гораздо более значимы и важны. Поэтому мы уделяем гораздо больше внимания типам на C ++.
Проверка кода
Мне придется поверить вам на слово, что так работают концепции. Я надеюсь, что это станет более доступным.
template<typename T>
concept Arithmetic = std::is_arithmetic_v<T>;
template<Arithmetic T>
struct Vector2D
{
Конечно, но в этом нет необходимости, если вы не определяете другие конструкторы (см. Ниже).
Vector2D() = default;
Конечно, но это не нужно, так как инициализаторы скобок работают и делают это за вас:
Vector2D(T x, T y);
Конечно: но не обязательно, здесь должен работать конструктор копирования по умолчанию.
Vector2D(const Vector2D<T> &other);
Не нужно объявлять эти inline
здесь. Никакого эффекта. Вы помещаете строку рядом с определением метода, чтобы убедиться, что компилятор не выдаст вам несколько предупреждений об определении.
inline T Magnitude() const;
inline Vector2D<T> Normal() const;
};
Отмечу, что inline
ключевое слово не имеет ничего общего с встраиванием кода. Компилятор решает это без какой-либо помощи инженера (потому что он лучше, чем вы).
Я бы упростил это до:
template<Arithmetic T>
struct Vector2D
{
T x;
T y;
T Magnitude() const;
Vector2D<T> Normal() const;
};
Арифметические операторы:
Есть дебаты о том, должны ли они быть членами или независимыми функциями. Либо это нормально. Я не буду упрекать вас за то, что вы поступили так, тогда как я сделал бы это иначе.
Есть аргумент в пользу того, чтобы сделать их автономными функциями. Но обычно это основано на симметрии при использовании операторов.
R = X + Y;
Если вы используете методы:
Если X есть Vector2D
и Y не является, тогда компилятор потенциально может преобразовать Y в Vector2D
и по-прежнему применяем операцию. Но если наоборот: Y есть Vector2D
и X нет, тогда компилятор не может преобразовать X в Vector2D
.
Если вы используете отдельно стоящие функции:
Тогда, если один параметр Vector2D
тогда другой может быть преобразован в Vector2D
и примененная функция. Это у вас симметричные преобразования.
Это нормально, если типы являются «арифметическими» по своей природе и могут легко преобразовать что угодно в ваш тип; это не тот случай с Vector2D
. Так что этот аргумент не выдерживает критики. Я бы также сказал, что это применимо к большинству типов; у вас должен быть очень хороший аргумент в пользу того, что ваш тип должен разрешать автоматическое преобразование (большинство типов этого не делают и используют explicit
на конструкторах с одним аргументом, чтобы предотвратить это).
Таким образом, здесь вы можете использовать любую технику, и обе подходят.
Я лично сделал бы их членами класса (единственная причина — добавить ясности, чтобы вы могли прочитать класс и увидеть все, что вы можете сделать, чтобы их изменить). Вы можете возразить, что можете добиться того же эффекта, добавив все объявления для арифметических операций как друзья к классу (или просто поместив объявления непосредственно под класс (и я не мог спорить с его).
Каким бы способом вы ни решили. Я бы поместил объявления рядом с классом и подальше от определения, чтобы было легко увидеть все возможные операции, применимые к классу.
Обычно мы реализуем operator+
с точки зрения operator+=
.
template<Arithmetic T>
inline Vector2D<T> operator+(const Vector2D<T> &first, const Vector2D<T> &second)
{
return Vector2D<T>(first.X + second.X, first.Y + second.Y);
}
Я бы написал так:
template<Arithmetic T>
inline Vector2D<T>& operator+=(Vector2D<T>& first, Vector2D<T> const& second)
{
first.X += second.X;
first.Y += second.Y;
return first;
}
template<Arithmetic T>
inline Vector2D<T> operator+(Vector2D<T> first, Vector2D<T> const& second)
{
// Note Pass first by value.
// This automatically gets us a new version of the object.
// The compiler will be able to detect if we can use move/copy
// automatically to get this move version.
//
// We can then do the += on this new copy.
// and perfectly return this value as output.
return first += second;
}
Это похоже на ошибку:
template<Arithmetic T>
inline Vector2D<T> operator-(const Vector2D<T> &first, const Vector2D<T> &second)
{
// Should you not subtract here:
return Vector2D<T>(first.X + second.X, first.Y + second.Y);
}
Мартин Йорк дал отличный обстоятельный ответ. Я согласен со всеми их пунктами.
Я хочу упомянуть функцию C ++ 20, о которой вы, возможно, не знали, а именно новые операторы сравнения, предоставляемые компилятором. Это сообщение в блоге подробно останавливается на деталях, но основные моменты:
- Операторы сравнения в C ++ 20 могут быть выведены компилятором для данного типа на основе
operator==
иoperator<=>
(оператор нового «космического корабля»).- Определение
operator==
для типа позволяет вывести неравенство (! =) для этого типа. - Определение
operator<=>
для типа позволяет вывести все операторы сравнения (<, <=,>,> =, ==,! =) для этого типа.
- Определение
- Определения, выведенные компилятором, являются
constexpr
иnoexcept
! - Также подразумевается порядок параметров (поэтому, если вы определите
Foo::operator==(int)
, ты получаешьFoo == int
,int == Foo
,Foo != int
, иint != Foo
все в одном). - Оба эти оператора могут быть
default
изд. Версия по умолчанию будет выполнять сравнение всех членов класса в порядке определения.
Что это значит для тебя? В твоей Vector2D
типа, вы можете объявить bool operator==(const Vector2D<T>&) const = default;
и компилятор автоматически сгенерирует оба operator==
и operator!=
.
(Очевидно, это работает только в том случае, если вы можете безопасно полагаться на операторы сравнения по умолчанию для членов вашего класса. В этом случае ваш operator==
и operator!=
уже используют сравнение по умолчанию для членов класса, так что это не проблема для вас.)
- 1
Мне известно об операторе <=>. Насколько я понимаю (фактически не использовал его раньше), я бы получил все операторы сравнения, включая <,>, <=,> = от него. Поскольку некоторые из них не имеют особого смысла для класса Vector2, я решил не использовать их. Не стесняйтесь поправлять меня, если я ошибаюсь или есть способ подавить нежелательных операторов.
— Эрик
- 1
Да, если вы по умолчанию
operator<=>
, вы получите все операторы равенства и сравнения. Но, как описывает @cariehl, вы можете просто определитьoperator==
чтобы получить все варианты (не) равенства и Только (неравенство. Если вам не нужны реляционные операции, просто не определяйтеoperator<=>
.— инди
Избавьтесь от всех конструкторов, конструкторы по умолчанию делают то же самое, и, учитывая, что C ++ 20 включает P0960, вы можете инициализировать структуры круглыми скобками.
Исправьте тип возврата оператора == и установите его по умолчанию.
Избавьтесь от оператора! =, Он генерируется автоматически.
std :: hypot — это вещь.
Если тип возврата не автоматический, вы можете пропустить его в операторе возврата:
return {-X, -Y};
По возможности выражайте операторы в терминах других операторов, чтобы минимизировать ошибки. Используйте @ = для определения @, используйте Vector * T для определения T * Vector и т. Д.
В частности, используйте
X operator @(X left, X const& right) { return left @= right; }
idiom, поскольку он автоматически использует правильный конструктор, копировать или перемещать для левого операнда, в зависимости от ситуации.Подумайте об ограничении типа T числами с плавающей запятой, иначе вы получите забавные величины и нормированные значения.
Напоследок напиши тесты, поймали бы
operator -
ошибка, например.
Определение
operator +
это плохая привычка. В данном случае это не имеет значения, ноfriend X operator +(X left, X const& right) { return left += right; }
копирует или перемещает левый операнд соответствующим образом, избегая ненужных копий в случае, если это было r-значение.— Роман Одайский
@RomanOdaisky Я исправил оператор +. Я правильно понял описание.
— Мартин Йорк
Да, хотя тело функции могло использовать
return
.— Роман Одайский
Кстати, C ++ 20 исправляет проблемы симметрии с операторами, определенными как функции-члены.
— Роман Одайский
«Между прочим, C ++ 20 действительно решает проблемы симметрии с операторами, определенными как функции-члены». Насколько мне известно, это только для операторов равенства / отношения. И это касается как членских, так и не членских версий.
— инди
Показать 7 больше комментариев