6
\$\begingroup\$

Overview

Using SDL and my own tiny wrapper library for it, I've written a tetris clone. I would really appreciate it if someone could take a look at it and tell me if something is particularly bad (or particularly good). I aim to make my code expressive, maintainable, with good abstraction levels, all the good stuff. Because the game is a small tetris clone, performance and such wasn't really a big concern. Also, you may notice that I say "matrice" instead of "matrix". Just don't be weirded out by that. :-)

The project has 1008 lines of code overall, and the wrapper library has ~750, but the library is quite intuitive and really it's only mentioned in the file that does drawing logic, and of course in the main file.

Tetris files:

game_logic.h:

#ifndef _GAME_LOGIC_H__
#define _GAME_LOGIC_H__

/*
 * Code for the actual game elements.
 */

#include "matrice.h"
#include "sdl-plus-plus/drawing.h"
#include "sdl-plus-plus/flow.h"
#include "boost/optional/optional.hpp"
#include <vector>
#include <random>
#include <chrono>
#include <cstddef>
#include <utility>
#include <array>
#include <cmath>
#include <unordered_map>

#include <iostream>

namespace Game_logic {
using Score = int;

/*
 * Rotation:
 * used with tetrominos to tell weather they should be rotated
 * clockwise or counter-clockwise.
 */
enum class Rotation : bool { Clockwise, Counter_clockwise };

enum class Angle { Zero = 0, Ninety = 90, One_eighty = 180, Two_seventy = 270 };

/*
 * Tetromino_block_offsets:
 * represents all the blocks of a tetromino offset from
 * it's current position, including (0,0)
 */
class Tetromino_block_offsets {
   public:
    using Blocks = std::unordered_map<Angle, Sdl::Point_vector>;

    explicit Tetromino_block_offsets(const Blocks&) noexcept;

    const Sdl::Point_vector& get() const noexcept;
    void rotate(Rotation) noexcept;
    void clear() noexcept;

   private:
    const Blocks* blocks_;  // This is a pointer instead of a reference
                            // so that assignment would be permitted.
                            // It is never null.
    Sdl::Point_vector no_points_{};
    Angle ang_{Angle::Zero};
};

Angle rotate_angle(Angle, Rotation) noexcept;
int first_quadrant(int) noexcept;

/*
 * Tetromino:
 * represents a complete tetromino,
 * including it's position and color.
 */
struct Tetromino {
    Tetromino(const Tetromino&) = default;
    Tetromino(Tetromino&&) = default;

    Tetromino& operator=(const Tetromino&) = default;
    Tetromino& operator=(Tetromino&&) = default;

    Sdl::Point pos_;
    Tetromino_block_offsets block_offsets_;
    SDL_Color color_;
};

/*
 * block_positions:
 * returns the block_positions that a tetrkomino takes up, i.e.,
 * every offset + the tetromino position
 */
Sdl::Point_vector block_positions(const Tetromino&);

using Optional_color = boost::optional<SDL_Color>;

constexpr std::size_t table_width = 8;
constexpr std::size_t table_height = 16;
constexpr std::size_t tetromino_count = 7;

/*
 * Tetromino_table:
 * all the positions taken up by tetromino blocks.
 */
class Tetromino_table {
   public:
    using Matrice = Util::Matrice<Optional_color, table_width, table_height>;
    using Row = Matrice::value_type;

    void insert(const Tetromino&) noexcept;
    /*
     * clear_rows:
     * check which rows are filled and clear them, returning the
     * number of rows cleared.
     */
    int clear_rows() noexcept;
    const Matrice& blocks() const noexcept;

   private:
    void clear_row(Row&) noexcept;
    /*
     * apply_gravity:
     * shifts all blocks down if they have empty
     * lines beneath them.
     */
    void apply_gravity() noexcept;
    void shift_row(std::size_t) noexcept;
    int clear_and_count_rows() noexcept;

    Matrice blocks_{};
};

/*
 * safe_move:
 * moves the tetromino by the given point,
 * making sure it doesn't exit the screen or
 * overlap with the table.
 */
void safe_move(Tetromino&, const Tetromino_table&, Sdl::Point) noexcept;

/*
 * safe_rotate:
 * does the same checking as safe_move, except
 * it rotates the tetromino.
 */
void safe_rotate(Tetromino&, const Tetromino_table&, Rotation) noexcept;

bool filled(const Tetromino_table::Row&) noexcept;
bool empty(const Tetromino_table::Row&) noexcept;

/*
 * touches_table:
 * tells weather a tetromino touches a table
 * depending on the given point parameter.
 *
 * example: touches_table(my_table, my_tetromino, { 0, 1 })
 * checks weather any block of my_tetromino has a block below it
 * in my_table.
 */
bool touches_table(const Tetromino_table&,
                   const Tetromino&,
                   Sdl::Point) noexcept;

/*
 * table_has_point:
 * checks if there's a block in the
 * table taking up the given point.
 */
bool table_has_point(const Tetromino_table&, Sdl::Point) noexcept;

/*
 * table_overlaps_tetromino:
 * check if the tetromino and the table overlap.
 */
bool table_overlaps_tetromino(const Tetromino_table&,
                              const Tetromino&) noexcept;

/*
 * overflown:
 * tells weather the table has been overflown with tetrominos,
 * usually meaning a game-over.
 */
bool overflown(const Tetromino_table&) noexcept;
bool on_screen(Sdl::Point) noexcept;
bool on_screen(const Tetromino&) noexcept;
bool touches_ground(const Tetromino&) noexcept;

enum class Tetromino_type { I, O, T, J, L, S, Z };

/*
 * Tetromino_factory:
 * creates random tetrominos on demand.
 */
class Tetromino_factory {
   public:
    Tetromino_factory();
    Tetromino create(Sdl::Point);

   private:
    const Tetromino_block_offsets::Blocks& random_blocks() const noexcept;
    SDL_Color random_color() const noexcept;
    void two_versions(Tetromino_type);
    void one_version(Tetromino_type);
    void add_block_proto(Tetromino_type, Angle, Sdl::Point_vector) noexcept;

    using Block_protypes =
        std::unordered_map<Tetromino_type, Tetromino_block_offsets::Blocks>;

    Block_protypes block_protos_{};
    std::vector<SDL_Color> color_protos_{Sdl::color_red(), 
                                         Sdl::color_blue(),
                                         Sdl::color_green(), 
                                         Sdl::color_yellow()};
    mutable std::random_device rd_{};
};
}  // End Game_logic

#endif

game_logic.cpp:

#include "game_logic.h"
#include <utility>
#include <algorithm>
#include <iterator>

namespace Game_logic {
namespace {
using Point_vector_iterator = Sdl::Point_vector::const_iterator;

using Predicate_f = std::function<bool(Sdl::Point)>;

using Outer_f = std::function<
    bool(Point_vector_iterator, Point_vector_iterator, Predicate_f)>;

bool apply_to_points(Outer_f outer,
                     const Tetromino& tetromino,
                     Predicate_f inner) {
    auto positions = block_positions(tetromino);

    return outer(positions.cbegin(), positions.cend(), inner);
}

bool any_of_points(const Tetromino& tetromino, Predicate_f f) {
    return apply_to_points(std::any_of<Point_vector_iterator, Predicate_f>,
                           tetromino, f);
}

bool all_of_points(const Tetromino& tetromino, Predicate_f f) {
    return apply_to_points(std::all_of<Point_vector_iterator, Predicate_f>,
                           tetromino, f);
}
}

Tetromino_block_offsets::Tetromino_block_offsets(const Blocks& blocks) noexcept
    : blocks_{&blocks} {}

const Sdl::Point_vector& Tetromino_block_offsets::get() const noexcept {
    return blocks_->at(ang_);
}

void Tetromino_block_offsets::rotate(Rotation rot) noexcept {
    ang_ = rotate_angle(ang_, rot);
}

Angle rotate_angle(Angle ang, Rotation rot) noexcept {
    auto ang_raw = static_cast<int>(ang);
    ang_raw += (rot == Rotation::Clockwise) ? -90 : 90;
    return static_cast<Angle>(first_quadrant(ang_raw));
}

int first_quadrant(int ang) noexcept {
    if (ang == -90) {
        return 270;
    }

    if (ang == 360) {
        return 0;
    }

    return ang;
}

Sdl::Point_vector block_positions(const Tetromino& tetromino) {
    auto result = tetromino.block_offsets_.get();

    for (auto& e : result) {
        e += tetromino.pos_;
    }

    return result;
}

void safe_move(Tetromino& tetromino,
               const Tetromino_table& tbl,
               Sdl::Point move_by) noexcept {
    tetromino.pos_ += move_by;

    if (!on_screen(tetromino) || touches_table(tbl, tetromino, {}))
        tetromino.pos_ -= move_by;
}

void safe_rotate(Tetromino& tetromino,
                 const Tetromino_table& tbl,
                 Rotation rot) noexcept {
    auto new_tetromino = tetromino;
    new_tetromino.block_offsets_.rotate(rot);

    if (on_screen(new_tetromino) &&
        !table_overlaps_tetromino(tbl, new_tetromino)) {
        tetromino = std::move(new_tetromino);
    }
}

void Tetromino_table::insert(const Tetromino& tetromino) noexcept {
    for (const auto& block_pos : block_positions(tetromino)) {
        if (on_screen(block_pos)) {
            Util::at(blocks_, block_pos) = tetromino.color_;
        }
    }
}

int Tetromino_table::clear_rows() noexcept {
    auto rows_cleared = clear_and_count_rows();

    if (rows_cleared > 0)
        apply_gravity();

    return rows_cleared;
}

auto Tetromino_table::blocks() const noexcept -> const Matrice& {
    return blocks_;
}

void Tetromino_table::clear_row(Row& row) noexcept {
    for (auto& block : row) {
        block = boost::none;
    }
}

void Tetromino_table::apply_gravity() noexcept {
    Util::for_each_row(blocks_, [this](auto n, auto& row) {
        if (n == 0 || !empty(row) || empty(Util::row(this->blocks_, n - 1)))
            return;

        this->shift_row(n - 1);
        this->clear_row(Util::row(blocks_, n - 1));
        this->apply_gravity();
    });
}

void Tetromino_table::shift_row(std::size_t n) noexcept {
    Util::row(blocks_, n + 1) = Util::row(blocks_, n);
}

int Tetromino_table::clear_and_count_rows() noexcept {
    int counter = 0;

    Util::for_each_row(blocks_, [this, &counter](auto, auto& row) {
        if (filled(row)) {
            ++counter;
            this->clear_row(row);
        }
    });

    return counter;
}

auto block_exists_lambda() noexcept {
    return [](const auto& block) { return block ? true : false; };
}

bool filled(const Tetromino_table::Row& row) noexcept {
    return std::all_of(row.cbegin(), row.cend(), block_exists_lambda());
}

bool empty(const Tetromino_table::Row& row) noexcept {
    return std::none_of(row.cbegin(), row.cend(), block_exists_lambda());
}

bool touches_table(const Tetromino_table& table,
                   const Tetromino& tetromino,
                   Sdl::Point relative) noexcept {
    return any_of_points(tetromino, [&table, relative](const auto& pos) {
        return table_has_point(table, pos + relative);
    });
}

bool table_has_point(const Tetromino_table& table, Sdl::Point point) noexcept {
    bool found = false;

    Util::for_each(table.blocks(),
                   [point, &found](auto pos, const auto& oc) -> void {
                       if (found || !oc)
                           return;

                       if (pos == point)
                           found = true;
                   });

    return found;
}

bool table_overlaps_tetromino(const Tetromino_table& tbl,
                              const Tetromino& tetromino) noexcept {
    return touches_table(tbl, tetromino, {0, 0});
}

bool overflown(const Tetromino_table& table) noexcept {
    /*
     * Overflow happens if there's a block (or multiple blocks)
     * in the topmost row.
     */
    const auto& top_row = Util::row(table.blocks(), 0);

    return std::any_of(top_row.cbegin(), top_row.cend(),
                       [](const auto& e) { return e ? true : false; });
}

bool on_screen(Sdl::Point point) noexcept {
    /*
     * Casting to an unsigned type is okay here, because we
     * first checked to make sure it's larger than 0.
     */

    return point.x >= 0 && static_cast<std::size_t>(point.x) < table_width;
}

bool on_screen(const Tetromino& tetromino) noexcept {
    return all_of_points(tetromino,
                         [](const auto& pos) { return on_screen(pos); });
}

bool touches_ground(const Tetromino& tetromino) noexcept {
    return any_of_points(
        tetromino, [](const auto& pos) { return pos.y == table_height - 1; });
}

Tetromino_factory::Tetromino_factory() {
    add_block_proto(Tetromino_type::I, Angle::Zero,
                    {{-2, 0}, {-1, 0}, {0, 0}, {1, 0}});

    add_block_proto(Tetromino_type::I, Angle::Ninety,
                    {{0, -2}, {0, -1}, {0, 0}, {0, 1}});

    add_block_proto(Tetromino_type::O, Angle::Zero,
                    {{0, 1}, {1, 0}, {0, 0}, {1, 1}});

    add_block_proto(Tetromino_type::S, Angle::Zero,
                    {{-1, 1}, {0, 1}, {0, 0}, {1, 0}});

    add_block_proto(Tetromino_type::S, Angle::Ninety,
                    {{0, -1}, {0, 0}, {1, 0}, {1, 1}});

    add_block_proto(Tetromino_type::T, Angle::Zero,
                    {{0, 1}, {0, 0}, {-1, 0}, {1, 0}});

    add_block_proto(Tetromino_type::T, Angle::Ninety,
                    {{0, -1}, {-1, 0}, {0, 1}, {0, 0}});

    add_block_proto(Tetromino_type::T, Angle::One_eighty,
                    {{1, 0}, {0, 0}, {-1, 0}, {0, -1}});

    add_block_proto(Tetromino_type::T, Angle::Two_seventy,
                    {{0, -1}, {0, 0}, {0, 1}, {1, 0}});

    add_block_proto(Tetromino_type::Z, Angle::Zero,
                    {{-1, 0}, {0, 0}, {0, 1}, {-1, 1}});

    add_block_proto(Tetromino_type::Z, Angle::Ninety,
                    {{0, 1}, {0, 0}, {1, 0}, {1, -1}});

    add_block_proto(Tetromino_type::J, Angle::Zero,
                    {{-1, 0}, {0, 0}, {1, 0}, {1, 1}});

    add_block_proto(Tetromino_type::J, Angle::Ninety,
                    {{0, 1}, {0, 0}, {0, -1}, {1, -1}});

    add_block_proto(Tetromino_type::J, Angle::One_eighty,
                    {{-1, -1}, {0, 0}, {-1, 0}, {1, 0}});

    add_block_proto(Tetromino_type::J, Angle::Two_seventy,
                    {{-1, 1}, {0, 1}, {0, 0}, {0, -1}});

    add_block_proto(Tetromino_type::L, Angle::Zero,
                    {{-1, 0}, {-1, 1}, {0, 0}, {1, 0}});

    add_block_proto(Tetromino_type::L, Angle::Ninety,
                    {{0, -1}, {0, 0}, {0, 1}, {1, 1}});

    add_block_proto(Tetromino_type::L, Angle::One_eighty,
                    {
                        {-1, 0}, {0, 0}, {1, 0}, {1, -1},
                    });

    add_block_proto(Tetromino_type::L, Angle::Two_seventy,
                    {{-1, -1}, {0, -1}, {0, 0}, {0, 1}});

    two_versions(Tetromino_type::I);
    one_version(Tetromino_type::O);
    two_versions(Tetromino_type::S);
    two_versions(Tetromino_type::Z);
}

Tetromino Tetromino_factory::create(Sdl::Point pos) {
    return {pos, Tetromino_block_offsets{random_blocks()}, random_color()};
}

const Tetromino_block_offsets::Blocks& Tetromino_factory::random_blocks() const
    noexcept {
    auto max = static_cast<int>(Tetromino_type::S);
    std::uniform_int_distribution<int> dist{0, max};

    return block_protos_.at(static_cast<Tetromino_type>(dist(rd_)));
}

SDL_Color Tetromino_factory::random_color() const noexcept {
    std::uniform_int_distribution<std::size_t> dist{0,
                                                    color_protos_.size() - 1};

    return color_protos_[dist(rd_)];
}

void Tetromino_factory::two_versions(Tetromino_type type) {
    auto& blocks = block_protos_[type];
    blocks[Angle::One_eighty] = blocks[Angle::Zero];
    blocks[Angle::Two_seventy] = blocks[Angle::Ninety];
}

void Tetromino_factory::one_version(Tetromino_type type) {
    auto& blocks = block_protos_[type];
    blocks[Angle::Ninety] = blocks[Angle::One_eighty] =
        blocks[Angle::Two_seventy] = blocks[Angle::Zero];
}

void Tetromino_factory::add_block_proto(Tetromino_type type,
                                        Angle ang,
                                        Sdl::Point_vector points) noexcept {
    block_protos_[type][ang] = std::move(points);
}
} // End Game_logic

matrice.h:

#ifndef _MATRICE_H__
#define _MATRICE_H__

#include "sdl-plus-plus/drawing.h"
#include <cstddef>
#include <array>
#include <functional>
#include <utility
/*
 * General utilities for dealing with matrices
 */
namespace Util {
template <class T, std::size_t W, std::size_t H>
using Matrice = std::array<std::array<T, W>, H>;

/*
 * Value_type:
 * quality of life alias. Saves me from writing
 * a lot of stuff in some declarations.
 */
template <class T>
using Value_type = typename T::value_type;

/*
 * Pres_value_type:
 * extracts the value type from the given class,
 * except if the given class is const,
 * the value type will be const too
 * (i.e., the constness is preserved).
 */
template <class, bool is_const>
struct Pres_value_type;

template <class From>
struct Pres_value_type<From, true> {
    using Type = const Value_type<From>;
};

template <class From>
struct Pres_value_type<From, false> {
    using Type = Value_type<From>;
};

/*
 * Pres_value_type_t:
 * a nice interface to the
 * Pres_value_type class.
 */
template <class From>
using Pres_value_type_t =
    typename Pres_value_type<From, std::is_const<From>::value>::Type;

/*
 * row:
 * accesses the nth row of the matrice.
 */
template <class M>
auto& row(M& matrice, std::size_t n) {
    return matrice.at(n);
}

/*
 * at:
 * accesses the matrice element at the position pos.
 */
template <class M>
auto& at(M& matrice, Sdl::Point pos) {
    return matrice.at(pos.y).at(pos.x);
}

/*
 * for_each_row:
 * apply f to each row.
 * The std::size_t parameter of f is the row number.
 */
template <class M>
void for_each_row(M& matrice,
                  std::function<void(std::size_t, Pres_value_type_t<M>&)> f) {
    for (std::size_t y = 0; y < matrice.size(); ++y) {
        f(y, row(matrice, y));
    }
}

/*
 * for_each:
 * apply f to each individual matrice element.
 * The Sdl::Point parameter of f is the element position.
 */
template <class M>
void for_each(M& matrice,
              std::function<void(Sdl::Point,
                                 Pres_value_type_t<Pres_value_type_t<M>>&)> f) {
    for_each_row(matrice, [f](auto y, auto& row) {
        for (std::size_t x = 0; x < row.size(); ++x) {
            f({static_cast<int>(x), static_cast<int>(y)}, row[x]);
        }
    });
}
}

#endif

visuals.h:

#ifndef _VISUALS_H__
#define _VISUALS_H__

#include "game_logic.h"
#include "sdl-plus-plus/drawing.h"
#include <cstddef>

/*
 * Code that has to do with drawing elements.
 */

namespace Visuals {
constexpr auto block_width = 20;
constexpr auto block_height = 20;

constexpr std::size_t window_width = block_width * Game_logic::table_width;
constexpr std::size_t window_height = block_height * Game_logic::table_height;

void draw_block(Sdl::Screen&, Sdl::Point, SDL_Color);
void draw_game_element(Sdl::Screen&, const Game_logic::Tetromino&);
void draw_game_element(Sdl::Screen&, const Game_logic::Tetromino_table&);
}

#endif

visuals.cpp:

#include "visuals.h"
#include "matrice.h"
#include <iostream>

namespace Visuals {
void draw_block(Sdl::Screen& screen, Sdl::Point pos, SDL_Color color) {
    pos *= Sdl::Point{block_width, block_height};

    screen.add_draw(Sdl::make_rect(pos, block_width, block_height), color,
                    Sdl::Color_filling::Filled);
}

void draw_game_element(Sdl::Screen& screen,
                       const Game_logic::Tetromino& tetromino) {
    for (const auto& t_block_pos : Game_logic::block_positions(tetromino)) {
        draw_block(screen, t_block_pos, tetromino.color_);
    }
}

void draw_game_element(Sdl::Screen& screen,
                       const Game_logic::Tetromino_table& table) {
    Util::for_each(table.blocks(), [&screen](auto pos, const auto& e) {
        if (e)
            draw_block(screen, pos, *e);
    });
}
}

game.h:

#ifndef _GAME_H__
#define _GAME_H__

#include "game_logic.h"
#include "visuals.h"
#include <map>
#include <functional>
#include <chrono>

/*
 * The game implementation,
 * basically the implementation of the game's
 * main loop. This file combines the game logic
 * and visuals.
 */

namespace Game_logic {
enum class Drop_mode : bool { Normal, Fast };

class Game_timer {
   public:
    explicit Game_timer(std::chrono::milliseconds) noexcept;

    static constexpr std::chrono::milliseconds min() noexcept {
        using namespace std::chrono_literals;
        return 50ms;
    }

    static constexpr std::chrono::milliseconds speed_boost() noexcept {
        using namespace std::chrono_literals;
        return 10ms;
    }

    bool ready() noexcept;
    void set_mode(Drop_mode) noexcept;
    void speed_up() noexcept;

   private:
    std::chrono::milliseconds delay_;
    Sdl::Timer timer_;
};

struct Button_dispatcher_data;

/*
 * Button_dispatcher:
 * a function class used to handle button presses.
 */
class Button_dispatcher {
   public:
    Button_dispatcher();
    void operator()(const SDL_Event&, Button_dispatcher_data) noexcept;

   private:
    using Dispatch_function =
        std::function<void(Button_dispatcher_data) noexcept>;
    using Dispatch_map = std::map<SDL_Scancode, Dispatch_function>;

    void do_dispatch(Dispatch_map&,
                     Button_dispatcher_data,
                     const SDL_Scancode&) noexcept;

    Dispatch_map keydown_dispatch_;
    Dispatch_map keyup_dispatch_;
};

/*
 * Button_dispatcher_data:
 * holds the data that may be modified when the user
 * presses/releases a button, depending on which button it was.
 */
struct Button_dispatcher_data {
    Tetromino& tetromino_;
    const Tetromino_table& tbl_;
    /*
     * This field is const because pressing a button
     * should never modify the table directly
     */
    Game_timer& game_timer_;
};

/*
 * Game:
 * represents the whole game process,
 * where the game advances a frame with
 * each call to frame_advance().
 */
class Game {
   public:
    explicit Game(std::chrono::milliseconds) noexcept;

    void frame_advance(const SDL_Event&);
    void frame_advance(Sdl::Screen&);

   private:
    void check_timer() noexcept;
    void handle_event(const SDL_Event&);
    void insert_tetromino() noexcept;
    void reset() noexcept;
    void check_game_over() noexcept;
    void handle_rows() noexcept;
    Tetromino create_tetromino() noexcept;

    Game_timer timer_;
    Button_dispatcher dispatch_button_{};
    Tetromino_factory t_factory_{};
    Tetromino_table t_table_{};
    Tetromino current_tetromino_{create_tetromino()};
    int score_{0};
    bool game_over_{false};
};
}

#endif

game.cpp:

#include "game.h"

namespace Game_logic {
Game_timer::Game_timer(std::chrono::milliseconds delay) noexcept
    : delay_{delay},
      timer_{delay} {}

bool Game_timer::ready() noexcept {
    return timer_.ready();
}

void Game_timer::set_mode(Drop_mode mode) noexcept {
    if (mode == Drop_mode::Normal) {
        timer_.set_delay(delay_);
    } else if (mode == Drop_mode::Fast) {
        timer_.set_delay(min());
    }
}

void Game_timer::speed_up() noexcept {
    if (delay_ > min()) {
        delay_ -= speed_boost();
    }
}

Button_dispatcher::Button_dispatcher()
    : keydown_dispatch_{
          {SDL_SCANCODE_LEFT, [](auto dispatcher_data) noexcept {
                                  safe_move(dispatcher_data.tetromino_,
                                            dispatcher_data.tbl_,
                                            {-1, 0});
}
}
, {SDL_SCANCODE_RIGHT,
   [](auto dispatcher_data) noexcept {
       safe_move(dispatcher_data.tetromino_, dispatcher_data.tbl_, {1, 0});
}
}
, {SDL_SCANCODE_X,
   [](auto dispatcher_data) noexcept {safe_rotate(dispatcher_data.tetromino_,
                                                  dispatcher_data.tbl_,
                                                  Rotation::Clockwise);
}
}
, {SDL_SCANCODE_Y,
   [](auto dispatcher_data) noexcept {safe_rotate(dispatcher_data.tetromino_,
                                                  dispatcher_data.tbl_,
                                                  Rotation::Counter_clockwise);
}
}
, {
    SDL_SCANCODE_DOWN, [](auto dispatcher_data) noexcept {
        dispatcher_data.game_timer_.set_mode(Drop_mode::Fast);
    }
}
}
, keyup_dispatch_{{SDL_SCANCODE_DOWN,
                   [](auto dispatcher_data) noexcept {
                       dispatcher_data.game_timer_.set_mode(Drop_mode::Normal);
}
}
}
{
    // Making sure that the same button does the same thing
    // on both English and German keyboards.
    keydown_dispatch_[SDL_SCANCODE_Z] = keydown_dispatch_[SDL_SCANCODE_Y];
}

void Button_dispatcher::operator()(
    const SDL_Event& event,
    Button_dispatcher_data dispatcher_data) noexcept {
    if (event.type == SDL_KEYDOWN) {
        do_dispatch(keydown_dispatch_, dispatcher_data,
                    event.key.keysym.scancode);
    } else if (event.type == SDL_KEYUP) {
        do_dispatch(keyup_dispatch_, dispatcher_data,
                    event.key.keysym.scancode);
    }
}

void Button_dispatcher::do_dispatch(Dispatch_map& dm,
                                    Button_dispatcher_data dispatcher_data,
                                    const SDL_Scancode& scancode) noexcept {
    if (dm.count(scancode) == 0)
        return;

    dm[scancode](dispatcher_data);
}

Game::Game(std::chrono::milliseconds delay) noexcept : timer_{delay} {}

void Game::frame_advance(const SDL_Event& event) {
    handle_event(event);
}

void Game::frame_advance(Sdl::Screen& screen) {
    check_timer();
    Visuals::draw_game_element(screen, current_tetromino_);
    Visuals::draw_game_element(screen, t_table_);
}

void Game::check_timer() noexcept {
    if (game_over_)
        return;

    if (!timer_.ready())
        return;

    if (touches_ground(current_tetromino_) ||
        touches_table(t_table_, current_tetromino_, {0, 1})) {
        insert_tetromino();
        handle_rows();
        reset();
        check_game_over();
        return;
    }

    current_tetromino_.pos_ += Sdl::Point{0, 1};
}

void Game::handle_event(const SDL_Event& event) {
    dispatch_button_(event, {current_tetromino_, t_table_, timer_});
}

void Game::insert_tetromino() noexcept {
    t_table_.insert(current_tetromino_);
}

void Game::reset() noexcept {
    current_tetromino_ = create_tetromino();
}

void Game::check_game_over() noexcept {
    if (table_overlaps_tetromino(t_table_, current_tetromino_)) {
        game_over_ = true;

        Sdl::Message_content content{};
        content.title = "Game over!";
        content.text = "Final score: " + std::to_string(score_) + ".\n";

        Sdl::show_message(content, Sdl::Message_box_type::Basic);
    }
}

void Game::handle_rows() noexcept {
    auto cleared_rows = t_table_.clear_rows();
    score_ += cleared_rows;

    for (int _ = 0; _ < cleared_rows; ++_) {
        timer_.speed_up();
    }
}

Tetromino Game::create_tetromino() noexcept {
    return t_factory_.create({table_width / 2, 0});
}
}

main.cpp:

#include "game_logic.h"
#include "game.h"
#include "sdl-plus-plus/drawing.h"
#include "sdl-plus-plus/flow.h"
#include <chrono>
#include <iostream>

int main() {
    using namespace Sdl;
    using namespace Game_logic;
    using namespace Visuals;
    using namespace std::literals::chrono_literals;

    Sdl_system_control _{};

    Screen_properties props{};
    props.width = window_width;
    props.height = window_height;
    props.title = "HELP";

    Screen screen{props};

    Game tetris{500ms};

    Event_loop ml{[&screen, &tetris](const auto& event) {
                      if (event.type == SDL_QUIT)
                          return Event_result::Quit;

                      tetris.frame_advance(event);
                      return Event_result::Continue;
                  },
                  [&screen, &tetris] {
                      tetris.frame_advance(screen);
                      screen.redraw(color_white());
                  }};

    ml.start();
}

I really don't think you need to look at the wrapper library (unless you actually want to or care), because as I said, it's quite intuitive. In case you do want to look at it:

Sdl wrapper library:

resource.h:

#ifndef _SDLPP__RESOURCE__HEADER__
#define _SDLPP__RESOURCE__HEADER__

#include "SDL2/SDL.h"
#include <stdexcept>
#include <memory>

namespace Sdl {
class Sdl_exception : public std::exception {
   public:
    const char* what() const noexcept override;
};

class Sdl_system_control {
   public:
    Sdl_system_control(Uint32 = SDL_INIT_EVERYTHING);
    ~Sdl_system_control() noexcept;

    // No copying or moving allowed
    Sdl_system_control(const Sdl_system_control&) = delete;
    Sdl_system_control(Sdl_system_control&&) = delete;
    Sdl_system_control& operator=(const Sdl_system_control&) = delete;
    Sdl_system_control& operator=(Sdl_system_control&&) = delete;
};

struct Window_deleter;
struct Renderer_deleter;
struct Surface_deleter;
struct Texture_deleter;

using Unique_window = std::unique_ptr<SDL_Window, Window_deleter>;
using Unique_renderer = std::unique_ptr<SDL_Renderer, Renderer_deleter>;
using Unique_surface = std::unique_ptr<SDL_Surface, Surface_deleter>;
using Shared_texture = std::shared_ptr<SDL_Texture>;

Shared_texture load_texture(const std::string&, Unique_renderer&);

// Exception checking
template <class T>
void check_pointer(const T&);
void check_function(int);

struct Window_deleter {
    void operator()(SDL_Window*) const noexcept;
};

struct Renderer_deleter {
    void operator()(SDL_Renderer*) const noexcept;
};

struct Surface_deleter {
    void operator()(SDL_Surface*) const noexcept;
};

struct Texture_deleter {
    void operator()(SDL_Texture*) const noexcept;
};

template <class T>
void check_pointer(const T& t_ptr) {
    if (t_ptr == nullptr)
        throw Sdl_exception{};
}
}

#endif

resource.cpp:

#include "resource.h"

namespace Sdl {
const char* Sdl_exception::what() const noexcept {
    return SDL_GetError();
}

Sdl_system_control::Sdl_system_control(Uint32 flags) {
    if (SDL_Init(flags) != 0)
        throw Sdl_exception{};
}

Sdl_system_control::~Sdl_system_control() noexcept {
    SDL_Quit();
}

Shared_texture load_texture(const std::string& name,
                            Unique_renderer& rend_point_pointerer) {
    auto surface = Unique_surface{SDL_LoadBMP(name.c_str())};
    check_pointer(surface);

    auto result = Shared_texture{
        SDL_CreateTextureFromSurface(rend_point_pointerer.get(), surface.get()),
        Texture_deleter{}};

    check_pointer(result);

    return result;
}

void check_function(int function_result) {
    if (function_result != 0)
        throw Sdl_exception{};
}

void Window_deleter::operator()(SDL_Window* wnd_ptr) const noexcept {
    SDL_DestroyWindow(wnd_ptr);
}

void Renderer_deleter::operator()(SDL_Renderer* rdr_ptr) const noexcept {
    SDL_DestroyRenderer(rdr_ptr);
}

void Surface_deleter::operator()(SDL_Surface* surf_ptr) const noexcept {
    SDL_FreeSurface(surf_ptr);
}

void Texture_deleter::operator()(SDL_Texture* txr_ptr) const noexcept {
    SDL_DestroyTexture(txr_ptr);
}
}

drawing.h:

#ifndef _SDLPP__DRAWING__HEADRER__
#define _SDLPP__DRAWING__HEADRER__

#include "SDL2/SDL.h"
#include "resource.h"
#include <string>
#include <vector>
#include <utility>
#include <map>
#include <functional>

namespace Sdl {
struct Point;
struct Canvas;
struct Line;

using Point_vector = std::vector<Point>;

struct Point {  // A 2D point, as simple as they make them
    int x;
    int y;
};  // TODO Maybe this shouldn't be a template...

constexpr bool operator==(Point, Point) noexcept;
constexpr bool operator!=(Point, Point) noexcept;
constexpr Point& operator+=(Point& lhs, Point rhs) noexcept;
constexpr Point& operator-=(Point& lhs, Point rhs) noexcept;
constexpr Point& operator*=(Point& lhs, Point rhs) noexcept;
constexpr Point operator+(Point lhs, Point rhs) noexcept;
constexpr Point operator-(Point lhs, Point rhs) noexcept;
constexpr Point operator*(Point lhs, Point rhs) noexcept;

enum class Message_box_type : Uint32 {
    Basic = 0,
    Error = SDL_MESSAGEBOX_ERROR,
    Warning = SDL_MESSAGEBOX_WARNING,
    Info = SDL_MESSAGEBOX_INFORMATION
};

struct Message_content {
    std::string title{};
    std::string text{};
};

void show_message(const Message_content&, Message_box_type);

constexpr SDL_Color color_red(Uint8 = 255, Uint8 alpha = 255) noexcept;
constexpr SDL_Color color_green(Uint8 = 255, Uint8 alpha = 255) noexcept;
constexpr SDL_Color color_blue(Uint8 = 255, Uint8 alpha = 255) noexcept;
constexpr SDL_Color color_yellow(Uint8 = 255, Uint8 alpha = 255) noexcept;
constexpr SDL_Color color_black(Uint8 alpha = 255) noexcept;
constexpr SDL_Color color_white(Uint8 alpha = 255) noexcept;

SDL_Rect make_rect(Sdl::Point, Sdl::Point) noexcept;
SDL_Rect make_rect(Sdl::Point, int w, int h) noexcept;

struct Screen_properties {  // Properties of a window
    int width{800};
    int height{600};
    Point position{SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED};
    std::string title{};
};

struct Canvas {  // A canvas is what you need in order to draw something
    Canvas() = default;
    explicit Canvas(const Screen_properties&);
    // Creates a canvas based on the given window properties

    Unique_window window{nullptr};
    Unique_renderer renderer{nullptr};
};

struct Angle_rad {  // Angle in radians
    double value{};
};

struct Line {
    Line() = default;
    Line(Point, Angle_rad, int) noexcept;
    Line(Point, Point) noexcept;

    Point from{};
    Point to{};
};

struct Drawable_texture {
    Shared_texture texture{nullptr};
    int width{};
    int height{};
};

class Texture_factory {
   public:
    void add(const std::string&, Unique_renderer&);
    bool exists(const std::string&) const noexcept;
    Drawable_texture get(const std::string&) const noexcept;

   private:
    std::map<std::string, Drawable_texture> m_map{};
};

enum class Color_filling : bool { Filled, None };

class Screen {  // Manipulates a canvas
   public:
    explicit Screen(const Screen_properties&);

    void add_draw(const std::string&, Point);
    void add_draw(Line, SDL_Color);
    void add_draw(SDL_Rect, SDL_Color, Color_filling);

    void redraw(SDL_Color);  // Clear, draw, render present

   private:
    using Snapshot = std::function<void(void)>;

    void set_renderer_color(SDL_Color);

    Canvas m_canvas{};
    Texture_factory m_factory{};
    // Stuff to draw
    std::vector<Snapshot> m_snapshots{};
};

// Basic_point operator definitions

constexpr bool operator==(Point lhs, Point rhs) noexcept {
    return lhs.x == rhs.x && lhs.y == rhs.y;
}

constexpr bool operator!=(Point lhs, Point rhs) noexcept {
    return lhs.x != rhs.x && lhs.y != rhs.y;
}

constexpr Point& operator+=(Point& lhs, Point rhs) noexcept {
    lhs.x += rhs.x;
    lhs.y += rhs.y;

    return lhs;
}

constexpr Point& operator-=(Point& lhs, Point rhs) noexcept {
    lhs.x -= rhs.x;
    lhs.y -= rhs.y;

    return lhs;
}

constexpr Point& operator*=(Point& lhs, Point rhs) noexcept {
    lhs.x *= rhs.x;
    lhs.y *= rhs.y;

    return lhs;
}

constexpr Point operator+(Point lhs, Point rhs) noexcept {
    return {lhs.x + rhs.x, lhs.y + rhs.y};
}

constexpr Point operator-(Point lhs, Point rhs) noexcept {
    return {lhs.x - rhs.x, lhs.y - rhs.y};
}

constexpr Point operator*(Point lhs, Point rhs) noexcept {
    return {lhs.x * rhs.x, lhs.y * rhs.y};
}

constexpr SDL_Color color_red(Uint8 amount, Uint8 alpha) noexcept {
    return {amount, 0, 0, alpha};
}

constexpr SDL_Color color_green(Uint8 amount, Uint8 alpha) noexcept {
    return {0, amount, 0, alpha};
}

constexpr SDL_Color color_blue(Uint8 amount, Uint8 alpha) noexcept {
    return {0, 0, amount, alpha};
}

constexpr SDL_Color color_yellow(Uint8 amount, Uint8 alpha) noexcept {
    return {amount, amount, 0, alpha};
}

constexpr SDL_Color color_black(Uint8 alpha) noexcept {
    return {0, 0, 0, alpha};
}

constexpr SDL_Color color_white(Uint8 alpha) noexcept {
    return {255, 255, 255, alpha};
}
}

#endif

drawing.cpp:

#include "drawing.h"
#include <cmath>
#include <iostream>

namespace Sdl {
// TODO fix passing unique pointer references around,
// start passing actual references to the required resources
void show_message(const Message_content& content, Message_box_type type) {
    check_function(SDL_ShowSimpleMessageBox(static_cast<Uint32>(type),
                                            content.title.c_str(),
                                            content.text.c_str(), nullptr));
}

SDL_Rect make_rect(Sdl::Point upper_left, Sdl::Point lower_right) noexcept {
    return {upper_left.x, upper_left.y, lower_right.x - upper_left.x,
            lower_right.y - upper_left.y};
}

SDL_Rect make_rect(Sdl::Point upper_left, int w, int h) noexcept {
    return {upper_left.x, upper_left.y, w, h};
}

Canvas::Canvas(const Screen_properties& properties)
    : window{SDL_CreateWindow(properties.title.c_str(),
                              properties.position.x,
                              properties.position.y,
                              properties.width,
                              properties.height,
                              SDL_WINDOW_SHOWN)},
      renderer{SDL_CreateRenderer(window.get(), -1, 0)} {
    check_pointer(window);
    check_pointer(renderer);
}

Line::Line(Point new_from, Angle_rad rads, int length) noexcept
    : from{new_from},
      to{static_cast<int>(std::sin(rads.value) * length + from.x),
         static_cast<int>(std::cos(rads.value) * length + from.y)}
// Sdl::Point_vector are integers in the SDL library, so narrowing
{  // has to happen
}

Line::Line(Point new_from, Point new_to) noexcept : from{new_from},
                                                    to{new_to} {}

void Texture_factory::add(const std::string& name, Unique_renderer& renderer) {
    Drawable_texture drawable = {load_texture(name.c_str(), renderer)};

    check_function(SDL_QueryTexture(drawable.texture.get(), nullptr, nullptr,
                                    &drawable.width, &drawable.height));

    m_map[name] = std::move(drawable);
}

bool Texture_factory::exists(const std::string& name) const noexcept {
    return m_map.count(name) != 0;
}

Drawable_texture Texture_factory::get(const std::string& name) const noexcept {
    return m_map.at(name);
}

Screen::Screen(const Screen_properties& properties) : m_canvas{properties} {
    check_function(SDL_SetRenderDrawBlendMode(m_canvas.renderer.get(),
                                              SDL_BLENDMODE_BLEND));
}

void Screen::add_draw(const std::string& name, Point where) {
    if (!m_factory.exists(name))
        m_factory.add(name, m_canvas.renderer);

    m_snapshots.push_back([this, name, where] {
        auto drawable = m_factory.get(name);

        SDL_Rect src{};
        SDL_Rect dst{};

        src.w = dst.w = drawable.width;
        src.h = dst.h = drawable.height;

        dst.x = where.x;
        dst.y = where.y;

        check_function(SDL_RenderCopy(m_canvas.renderer.get(),
                                      drawable.texture.get(), &src, &dst));
    });
}

void Screen::add_draw(Line line, SDL_Color color) {
    m_snapshots.push_back([this, line, color] {
        set_renderer_color(color);

        check_function(SDL_RenderDrawLine(m_canvas.renderer.get(), line.from.x,
                                          line.from.y, line.to.x, line.to.y));
    });
}

void Screen::add_draw(SDL_Rect rect, SDL_Color color, Color_filling filling) {
    m_snapshots.push_back([this, rect, color, filling] {
        set_renderer_color(color);

        check_function(SDL_RenderDrawRect(m_canvas.renderer.get(), &rect));

        // do check_function
        if (filling == Color_filling::Filled) {
            SDL_RenderFillRect(m_canvas.renderer.get(), &rect);
        }
    });
}

void Screen::redraw(SDL_Color color) {
    set_renderer_color(color);
    check_function(SDL_RenderClear(m_canvas.renderer.get()));

    for (const auto& snapshot : m_snapshots)
        snapshot();

    SDL_RenderPresent(m_canvas.renderer.get());
    m_snapshots.clear();
}

void Screen::set_renderer_color(SDL_Color color) {
    check_function(SDL_SetRenderDrawColor(m_canvas.renderer.get(), color.r,
                                          color.g, color.b, color.a));
}
}

flow.h:

#ifndef _SDLPP__FLOW__HEADER__
#define _SDLPP__FLOW__HEADER__

#include "drawing.h"
#include <functional>
#include <chrono>
#include <stdexcept>
#include <thread>
#include <atomic>

namespace Sdl {
bool key_down(SDL_Scancode);

class Basic_loop {
   public:
    using Function = std::function<void(void)>;

    explicit Basic_loop(Function) noexcept;
    void start();

   private:
    Function m_f{};
};

enum class Event_result : bool { Quit, Continue };

// Maybe add a Pump_event_loop class?

class Event_loop {
   public:
    using Event_function = std::function<Event_result(const SDL_Event&)>;
    using Unconditional_function = std::function<void(void)>;

    Event_loop(Event_function, Unconditional_function) noexcept;
    void start();

   private:
    Event_function m_event_f{};
    Unconditional_function m_unconditional_f{};
    SDL_Event m_event{};
};

class Thread_manager {
   public:
    using Quit_flag = std::atomic<bool>;
    using Thread_function = std::function<void()>;
    using Graphics_init_function = std::function<Screen()>;
    using Graphics_thread_function = std::function<void(Screen&)>;

    void add_graphics_thread(Graphics_init_function,
                             Graphics_thread_function f);
    void add_thread(Thread_function f);
    void start_all();

   private:
    Quit_flag quit_{false};
    std::vector<std::thread> threads_{};
};

// These loops can share a common concept in c++17

// TODO maybe an option to change the timers delay?
class Timer {
   public:
    explicit Timer(std::chrono::milliseconds) noexcept;
    /*
     * The user is expected to give a sane value, larger than 0,
     * not overflowing Uint32. Because small values are pretty much expected,
     * none of this is actually checked, so beware or check it yourself.
     */

    bool ready() noexcept;

    std::chrono::milliseconds get_delay() const noexcept;
    void set_delay(std::chrono::milliseconds) noexcept;

   private:
    Uint32 delay_{};
    Uint32 current_{SDL_GetTicks()};
};
}

#endif

flow.cpp:

#include "flow.h"
#include "SDL2/SDL.h"

namespace Sdl {
bool key_down(SDL_Scancode scancode) {
    return SDL_GetKeyboardState(nullptr)[scancode];
}

Basic_loop::Basic_loop(Function f) noexcept : m_f{f} {}

void Basic_loop::start() {
    while (!SDL_QuitRequested()) {
        m_f();
    }
}

Event_loop::Event_loop(Event_function event_f,
                       Unconditional_function unconditional_f) noexcept
    : m_event_f{event_f},
      m_unconditional_f{unconditional_f} {}

void Event_loop::start() {
    while (true) {
        while (SDL_PollEvent(&m_event)) {
            if (m_event_f(m_event) == Event_result::Quit)
                return;  // Quit if the user says so
        }

        m_unconditional_f();
    }
}

void Thread_manager::add_graphics_thread(Graphics_init_function init,
                                         Graphics_thread_function f) {
    threads_.push_back(std::thread{[this, init, f] {
        auto screen = init();

        while (!quit_) {
            SDL_PumpEvents();
            f(screen);
            if (SDL_QuitRequested()) {
                quit_ = true;
            }
        }
    }});
}

void Thread_manager::add_thread(Thread_function f) {
    threads_.push_back(std::thread{[f, this] {
        while (!quit_) {
            f();
        }
    }});
}

void Thread_manager::start_all() {
    for (auto& thread : threads_) {
        thread.join();
    }
}

Timer::Timer(std::chrono::milliseconds delay) noexcept {
    set_delay(delay);
}

bool Timer::ready() noexcept {
    auto ticks = SDL_GetTicks();

    if (ticks >= delay_ + current_) {
        current_ = ticks;
        return true;
    }

    return false;
}

std::chrono::milliseconds Timer::get_delay() const noexcept {
    return std::chrono::milliseconds{delay_};
}

void Timer::set_delay(std::chrono::milliseconds delay) noexcept {
    delay_ = static_cast<Uint32>(delay.count());
}
}
\$\endgroup\$
  • 1
    \$\begingroup\$ I would have seporated the SDL wrapper and the game into separate code reviews. \$\endgroup\$ – Martin York Jan 19 '17 at 20:54
  • 1
    \$\begingroup\$ You should read: What are the rules about using an underscore in a C++ identifier? \$\endgroup\$ – Martin York Jan 19 '17 at 20:57
  • 1
    \$\begingroup\$ Leading underscore followed by an uppercase letter and double underscore. _GAME_LOGIC_H__. All these identifiers are reserved for use by the implementation. As such using them is UB. \$\endgroup\$ – Martin York Jan 19 '17 at 22:21
  • 1
    \$\begingroup\$ You are using boost for optional but c++17 provides that: en.cppreference.com/w/cpp/utility/optional \$\endgroup\$ – AntonioCS Mar 6 '18 at 19:37
  • 1
    \$\begingroup\$ @AntonioCS Thanks for the info, but check the tags - the code is written for c++14. \$\endgroup\$ – Enn Michael Mar 13 '18 at 11:56
4
\$\begingroup\$

Oh wow, that's some really nice C++ code :)

Here are some few things you could do/or not, depends:

  • Lambdas can deduce their return type, so the -> void is unnecessary:

    Util::for_each(table.blocks(),
                   [point, &found](auto pos, const auto& oc) /*-> void*/ {
                       if (found || !oc)
                           return;
    
                       if (pos == point)
                           found = true;
                   });
    
  • Don't recreate std::uniform_int_distribution every time you need a random number, instead, just like std::random_device, create it once and use it multiple times. Quick fix: make it static :)

  • You're not consistent with function declarations in your class definitions:

    Event_loop(Event_function, Unconditional_function) noexcept; // no names
    void add_graphics_thread(Graphics_init_function,
                         Graphics_thread_function f); // name!!
    

    Some of them have names, some don't and some have both.

  • Personally, I think those *_deleter functors are unnecessary, but the library is not as flexible without them, so you choose.

  • You can abbreviate std::function<void(void)> to std::function<void()>

  • Remove TODOs when you are done with them:

    // TODO Maybe this shouldn't be a template...
    

    Point is not a template...

  • You might like aggregate initialization without arguments, but this is only necessary for built-ins, classes without user defined constructor, ... (like Point) (more info). std::string title{}; can be safely replaced with std::string title;

  • Operators can be implemented in terms of other operators, which is better (less code -> maintenance) and shorter to write:

    constexpr bool operator!=(Point lhs, Point rhs) noexcept {
        // return lhs.x != rhs.x && lhs.y != rhs.y;
        return !(lhs == rhs);
    }
    

    Same with the other operators.

  • You are using a lot of capturing lambdas in your code, which are saved using a std::function. This is not ideal, as usage of std::function not only slows down the program, but also the compilation time due to type-erase. See this for more info. There isn't really an alternative though, so maybe use it sparingly?

  • std::mt19937 is faster than calls to std::random_device, so consider using that instead.

  • Destructors are implicitly noexcept, so you don't need to specify it explicitly: ~Sdl_system_control() /*noexcept*/;

\$\endgroup\$
  • \$\begingroup\$ Thank you very very much :-). I would just like to mention how good advice "Operators can be implemented in terms of other operators, which is better" is, because yesterday I found that the implementation return lhs.x != rhs.x && lhs.y != rhs.y; actually has a bug - it uses &&, but it should use || instead - and this would have been avoided if I just implemented it in terms of operator== like you said. \$\endgroup\$ – Enn Michael Jan 19 '17 at 20:02

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.