Design, test, and build our own variant class!
This assignment has three parts:
- First, you're going to implement your own version of a
variant
class. - Second, you're going to implement tests to ensure that your class works correctly -- then check it into git.
- The third part of this assignment is a "code review" -- where each student will code-review another student's work.
C++ is a strongly-typed language. C++17 introduced support for dynamically-typed variables. As we discussed in lecture, the std::variant
and std::any
types give your C++ program considerable flexibility with few limitations .
For instance, when using std::variant
, you can specify the types you want to support like so:
// This variant can hold an int, float or a std::string.
std::variant<int, float, std::string> theVariant;
std::variant
-based variables can hold any value of the specified types -- but only one at a time. It can be an int
or a float
, but not both simultaneously. Once you assign a value, the internal type of the variant
changes accordingly. You ask a std::variant
variable what type of value it currently holds -- which is represents as an ordinal value (0..n
).
In this assignment, you're going to build your own (simplified) version of a Variant
class. This will help you understand C++ better, as well as give you some practice with software design, testing, and code reviews.
For this assignment, you may NOT use the C++
std::variant
class as part of your design.
Your Variant
class must be able to hold any of the following types: int
, float
, const char*
(or std::string
). You won't be asked to store user defined types (classes).
Consider using an enum class
to indicate which type your Variant
is currently holding:
enum class VariantType { intType, floatType, stringType };
Your Variant
class must define all the standard methods required by any class (discussed in lecture). In addition, you'll need one constructor for each allowable type:
class Variant
{
// Add OCF methods...
Variant(int aValue);
Variant(float aValue);
Variant(const char* aValue);
};
NOTE: When storing values of type
const char*
-- consider whether you can store the pointer you're given, or whether you need to do a deep copy.
You will implement the corresponding setter methods (operators):
Variant& operator=(int aValue);
Variant& operator=(float aValue);
Variant& operator=(const char* aValue);
NOTE: When storing values of type
const char*
-- consider whether you can store the pointer you're given, or whether you need to do a deep copy.
Consider calling these assignment operators to initialize your Variant
in your constructors to reduce code duplication.
Given a Variant
variable, a caller may request a value from the Variant
in any of the four given types:
std::optional<int> asInt() const; // Retrieve value as int
std::optional<float> asFloat() const; // Retrieve value as float
std::optional<std::string> asString() const; // Retrieve value as string
VariantType getType() const; // Get current type of variant
Notice two things in the getters above:
- The return type of
std::optional<...>
- The
const
keyword at the end of the declarations.
The std::optional
is there to allow for error handling. Unlike the built-in std::variant
, we want you to implement implicit type conversions. This means that, when possible, your Variant
class should attempt to automatically convert between types. Here are some examples:
Variant intVariant(42);
intVariant.asInt(); // Should return 42 (an int)
intVariant.asFloat(); // Should return 42.0f (a float)
intVariant.asString(); // Should return "42" (a string)
intVariant.getType(); // Should return VariantType::intType
Variant floatVariant(3.14f);
floatVariant.asInt(); // Should return 3 (rounded to the nearest int)
floatVariant.asFloat(); // Should return 3.14f (a float)
floatVariant.asString(); // Should return "3.14" (a string)
floatVariant.getType(); // Should return VariantType::floatType
Variant stringVariant1("hello");
stringVariant1.asInt(); // Should return std::nullopt (can't convert)
stringVariant1.asFloat(); // Should return std::nullopt (can't convert)
stringVariant1.asString(); // Should return "hello" (a string)
stringVariant1.getType(); // Should return VariantType::stringType
Variant stringVariant2("123");
stringVariant2.asInt(); // Should return 123 (an int)
stringVariant2.asFloat(); // Should return 123.0f (a float)
stringVariant2.asString(); // Should return "123" (a string)
stringVariant2.getType(); // Should return VariantType::stringType
The const
keyword mentioned earlier specifies that the internals of the Variant
object should not change when a getter is called. Meaning the value nor the type should change when calling a getter.
Objects of type Variant
must be comparable. Implement all the basic comparison operators (==
, !=
, <
, <=
, >
, >=
). Remember: programmers are lazy, and prefer to write as little code as possible. Can you think of a way to write the least amount of code for your comparison operators?
Once again, just like for the getters, we want you to implement implicit conversions between types. If your getters are implemented correctly, this should be simple. Here is an example:
Variant intVariant(20);
Variant floatVariant(40.0f);
intVariant < floatVariant; // Should return 'true', as 20 < 40
The last feature to implement is to make your variable "output streamable". Consider:
Variant theVariant("hello world");
std::cout << theVariant;
Implement this feature using the standard friend
method (already present in the starter code):
friend std::ostream& operator<<(std::ostream& aStream, const Variant& aVar);
Our auto-grader provides some basic validation of your code. But that's not a full test suite.
Your final task is to make your Variant
class testable. In lecture, we provide a video that detailed how to use a simple testing framework called Testable
. That class has been included in your assignment repository. You're welcome to use that -- or the much more powerful gtest
class provided by Google. Regardless of which you choose, in this step, you'll write tests to confirm that your Variant
class is working as designed. The thoroughness and quality of the tests you write will be evaluated as part of your score for this assignment.
You should create a test for every method in the Variant
interface. Sometimes it's convenient to write a single test method to test multiple things, but generally we write one test per method. See our video about designing a testing system for how to integrate our Testable
class into your solution. It's pretty easy to do.
As we discussed in lecture, it may also be easier to create a subclass of Variant
(e.g. TestVariant
) and put all your testing code and logic here. That keeps your original Variant
class from test-related changes.
Turn in your submission by pushing your work to GitHub. Each time you do so, your code will be auto-tested by, "Vlad-the-compiler". The results are visible on your GitHub page for the assignment. Vlad didn't get invited to any new-year's eve parties -- and is a bad mood. Make sure you turn your work in on time!
Also make sure that all of your code (.h
and .cpp
files) are in the Source/
directory. Vlad will only compile code in this directory!
Section | Points |
---|---|
Compile test | 20 |
Compare test | 25 |
Values test | 30 |
Manual code review | 25 |
In your project folder is a file called, "students.json". For each assignment, fill out the fields in that file, including your name, PID, and level of effort for the assignment.