Шаблон C ++ 20 Vector2D

Довольно простой шаблон 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 ответа
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);
    }

  • 1

    Определение operator + это плохая привычка. В данном случае это не имеет значения, но friend X operator +(X left, X const& right) { return left += right; } копирует или перемещает левый операнд соответствующим образом, избегая ненужных копий в случае, если это было r-значение.

    — Роман Одайский

  • @RomanOdaisky Я исправил оператор +. Я правильно понял описание.

    — Мартин Йорк

  • Да, хотя тело функции могло использовать return.

    — Роман Одайский

  • Кстати, C ++ 20 исправляет проблемы симметрии с операторами, определенными как функции-члены.

    — Роман Одайский

  • «Между прочим, C ++ 20 действительно решает проблемы симметрии с операторами, определенными как функции-члены». Насколько мне известно, это только для операторов равенства / отношения. И это касается как членских, так и не членских версий.

    — инди

Мартин Йорк дал отличный обстоятельный ответ. Я согласен со всеми их пунктами.

Я хочу упомянуть функцию 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<=>.

    — инди

  1. Избавьтесь от всех конструкторов, конструкторы по умолчанию делают то же самое, и, учитывая, что C ++ 20 включает P0960, вы можете инициализировать структуры круглыми скобками.

  2. Исправьте тип возврата оператора == и установите его по умолчанию.

  3. Избавьтесь от оператора! =, Он генерируется автоматически.

  4. std :: hypot — это вещь.

  5. Если тип возврата не автоматический, вы можете пропустить его в операторе возврата: return {-X, -Y};

  6. По возможности выражайте операторы в терминах других операторов, чтобы минимизировать ошибки. Используйте @ = для определения @, используйте Vector * T для определения T * Vector и т. Д.

  7. В частности, используйте X operator @(X left, X const& right) { return left @= right; } idiom, поскольку он автоматически использует правильный конструктор, копировать или перемещать для левого операнда, в зависимости от ситуации.

  8. Подумайте об ограничении типа T числами с плавающей запятой, иначе вы получите забавные величины и нормированные значения.

  9. Напоследок напиши тесты, поймали бы operator - ошибка, например.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *