Homework 1 Notes
1. Introduction
This quarter, we're diving into a large-scale project focused on 2D Vector Graphics. We'll develop a basic vector graphics library under the namespace VG
. Due to academic honesty policies, I won't be able to make the repository public but will document the key aspects of the project here.
In this homework, we'll be implementing two classes:
VG::Point
: This class will serve as a data structure for storing 2D coordinates.VG::VectorGraphic
: This class will hold a collection ofVG::Point
s and will also specify whether the shape is closed.
2. Implementing the VG::Point
Class
2.1 Introduction
While VG::Point
initially appears to be a straightforward data structure for storing 2D coordinates, the assignment specification encourages us to think about future extensibility. It notes that 2D shapes can be decomposed into points, lines, and curves. Importantly, a point in this system is not just a 2D coordinate; it could also possess "handles" to help define curves. Given this added complexity, a class structure is more suited for VG::Point
than a simple struct would be.
2.2 Implementation
Currently, VG::Point
supports basic operations like ==
, !=
, <<
, and getters.
#pragma once
#include <iosfwd>
namespace VG
{
class Point
{
public:
constexpr Point(int x, int y) : myX{ x }, myY{ y } { }
Point(const Point& other) = default;
Point(Point&& other) = default;
~Point() = default;
Point& operator=(const Point&) = default;
Point& operator=(Point&&) = default;
bool operator==(const Point&) const;
bool operator!=(const Point&) const;
constexpr int getX() const { return myX; }
constexpr int getY() const { return myY; }
private:
int myX{ 0 };
int myY{ 0 };
};
std::ostream& operator<<(std::ostream& os, const Point& p);
}
Several things to note:
- The "handles" mentioned in the assignment spec are not implemented.
- The
<iosfwd>
header is used instead of<iostream>
to avoid including the entire<iostream>
header. This is a good practice to avoid unnecessary compilation time. - The "Rule of Five" is used to implement the copy and move constructors and assignment operators.
- The
constexpr
keyword is used to mark the constructor and getters asconstexpr
to allow the compiler to evaluate them at compile time.
Point.cpp
contains the implementation of the Point
class. It is trivial and not worth mentioning here.
3. Implementing the VG::VectorGraphic
Class
3.1 Introduction
A VectorGraphic is a collection of VG::Point
s with option specifying if the shape is closed or not. "Close" here simply mean drawing an extra line from the last point to the first point. In the later parts of the project, we will also implement properties like fill and stroke, etc.
3.2 Design Considerations
-
Should
VG::Point
be passed by value or by reference?: The common wisdom tells us to keep it simple: if an object acts just like a simple type (like a number), then make it a value object. Otherwise, treat it as a reference object. Since we're considering extensibility (like the possibility of adding "handles" for curves in the future), using references for Point objects fits well here. And since we are usingconst
references, we don't really need to worry about the performance penalty of copying objects. -
Handling Open and Closed Shapes: The design needs to accommodate both open and closed shapes. Instead of using boolean accessors like
setOpen(true)
orsetClosed(false)
, which can be confusing and reveal implementation details, it's better to use separate methods for this purpose, such asopenShape()
andcloseShape()
. However, they could mislead clients into thinking they need to pass in a shape object as a parameter, which is a naming issue that needs to be considered. To allow clients to check the state of a shape, providing methods likeisOpen()
andisClosed()
is beneficial. -
Reporting Bounds and Horizontal Alignment: While vector graphics can theoretically have various complex bounds, in practical usage, most systems prefer a simple rectangular representation aligned horizontally. This rectangular "box" is usually defined by its width and height. This simplification serves most client needs, like memory allocation for drawing, where the client would multiply the width by the height to create a buffer. This approach also avoids complications that could arise from other definitions of a rectangle, focusing instead on the most commonly understood version with horizontal alignment.
3.3 Header File
In light of the design considerations above, the VG::VectorGraphic
class is implemented as follows:
#pragma once
#include "Point.h"
#include <vector>
namespace VG
{
using Points = std::vector<Point>;
class VectorGraphic
{
public:
VectorGraphic();
VectorGraphic(const VectorGraphic& other) = default;
VectorGraphic(VectorGraphic&& other) = default;
~VectorGraphic() = default;
VectorGraphic& operator=(const VectorGraphic&) = default;
VectorGraphic& operator=(VectorGraphic&&) = default;
bool operator==(const VectorGraphic& other) const;
bool operator!=(const VectorGraphic& other) const;
void addPoint(const Point& p); // pass by const reference
void addPoint(Point&& p); // pass by rvalue reference
void removePoint(const Point& p);
void erasePoint(int index);
void openShape();
void closeShape();
bool isOpen() const;
bool isClosed() const;
int getWidth() const;
int getHeight() const;
size_t getPointCount() const;
const Point& getPoint(int index) const;
private:
Points myPath;
enum class ShapeStyle { Open, Closed } myShapeStyle;
};
}
Several things to note:
- The
Points
type is defined as an alias forstd::vector<Point>
. This is a good practice to avoid typing out the full type name and to make the code more readable. - The
ShapeStyle
enum is defined inside the class to avoid polluting the global namespace. - GetPointCount return type is
size_t
instead ofint
becausesize_t
is the type used by the standard library for sizes and indices.
3.4 Source File
I won't show the full implementation of VectorGraphic.cpp
here, but I'll highlight some interesting functions:
VectorGraphic::addPoint(Point &&p)
:
void VectorGraphic::addPoint(Point &&p)
{
myPath.emplace_back(p);
// trivially-copyable type, don't need to std::move
}
The Point is passed by r-value reference, which allows us to move the Point into the vector instead of copying it. However, since Point is a trivially-copyable type, the compiler will automatically optimize the copy into a move, so we don't need to explicitly call std::move()
. Also, if you forget, r-value reference parameters are those that are passed like this:
myVG.addPoint(Point{ 1, 2 });
R-value is a new feature in C++11 so that we can access temporary objects by reference. This is useful for avoiding unnecessary copies.
VectorGraphic::removePoint(const Point& p)
:
void VectorGraphic::removePoint(const Point &p)
{
// myPath.erase(std::remove_if(myPath.begin(), myPath.end(),
// [&p](const auto &point)
// { return point == p; }),
// myPath.end());
// A more modern and concise way to remove elements from a container:
std::erase_if(myPath,
[&p](const auto &point)
{ return point == p; });
}
As said in the comments, the std::erase_if()
function is a more modern and concise way to remove elements from a container. It is available in C++20. We used to have to use std::remove_if()
in conjunction with std::erase()
to remove elements from a container. The std::erase_if()
function combines these two steps into one.
VectorGraphic::getWidth()
andVectorGraphic::getHeight()
:
int VectorGraphic::getWidth() const
{
if (myPath.empty())
{
return 0;
}
auto minMaxX = std::minmax_element(myPath.begin(), myPath.end(),
[](const auto &lhs, const auto &rhs)
{ return lhs.getX() < rhs.getX(); });
return minMaxX.second->getX() - minMaxX.first->getX();
}
int VectorGraphic::getHeight() const
{
if (myPath.empty())
{
return 0;
}
auto minMaxY = std::minmax_element(myPath.begin(), myPath.end(),
[](const auto &lhs, const auto &rhs)
{ return lhs.getY() < rhs.getY(); });
return minMaxY.second->getY() - minMaxY.first->getY();
}
These two functions are implemented using the std::minmax_element()
algorithm. This algorithm returns a pair of iterators pointing to the smallest and largest elements in the range. We can then use the getX()
and getY()
methods to get the x and y coordinates of the smallest and largest points. The width and height are then calculated by subtracting the x and y coordinates of the smallest point from the largest point.