Lab Exercise 2: Arrays
We will build a unit test suite that shows that C-style raw arrays may be replaced without any loss of function or behaviour using higher level constructs made available by the C++ Standard Template Library
Instructions
These instructions cover creating a new project in Microsoft Visual Studio. The source files used in this exercise may be configured in any other IDE/platform of choice to achieve the same results.
Using std::array
We will use a std::array
to replace a raw C-style array.
- Create a new empty Visual Studio C++ project named array.
- Right-click the array solution on the left navigation pane and choose Properties
- Expand the Linker node and select the System item
- Select
“Console (/SUBSYSTEM:CONSOLE)
” from the SubSystem drop-down menu. This will allow us to view the results of running the application in the DOS Command window.
- Download the Catch unit testing framework header file and add to the project Header Files section.
- Add a new header file named
Point
to the project. We will declare a simple structure namedPoint
, and add some useful functions to support our structure. We will use the Point structure to exercise array of objects and their equivalents.
#pragma once
#include <iostream>
#include <ostream>
#define VALUES { csc240::Point{0.0, 0.0}, csc240::Point{1.0, 1.0}, csc240::Point{2.0, 2.0} }
namespace csc240
{
template <typename T, unsigned S>
inline unsigned arraysize( const T( &v )[S] ) { return S; }
struct Point { double x; double y; };
inline std::ostream& operator << ( std::ostream& stream, const Point& point )
{
stream << "Point - x: " << point.x << ", y: " << point.y << std::endl;
return stream;
}
inline bool operator == ( const Point& left, const Point& right )
{
return ( left.x == right.x ) && ( left.y == right.y );
}
}
- Add a new C++ source file named
array
to the project. We will build a BDD style test that illustrates that we can use astd::array
in exactly the same way as a C-array. The scenario for the test will be that we can replace any C-style array with astd::array
instance. We will then test for a few common use cases such as initialising the array, iterating over values in the array, updating values in the array etc. Note: We chose to use the modernusing
syntax for defining an alias over the old Ctypedef
.
#define CATCH_CONFIG_MAIN
#include "catch.hpp"
#include "Point.h"
#include <array>
#include <iterator>
#include <sstream>
#include <type_traits>
using RawArray = csc240::Point[];
template <unsigned S> using ModernArray = std::array<csc240::Point, S>;
SCENARIO( "Raw C-style arrays may be replaced with std::array" )
{
GIVEN( "A raw C array of Point instances" )
{
const RawArray rawArray = VALUES;
const auto n = std::extent< decltype( rawArray ) >::value;
REQUIRE( n == csc240::arraysize( rawArray ) );
WHEN( "Represented as a std::array" )
{
const ModernArray<3> stdArray VALUES;
THEN( "Size should be same" )
{
REQUIRE( n == stdArray.size() );
}
THEN( "Sub-script operator should yield similar results" )
{
for ( int i = 0; i < n; ++i )
{
REQUIRE( rawArray[i] == stdArray[i] );
}
}
THEN( "Pointer arithmetic works identically" )
{
for ( int i = 0; i < n; ++i )
{
REQUIRE( *( rawArray + i ) == *( stdArray.data() + i ) );
REQUIRE( *( rawArray + i ) == *( &stdArray[0] + i ) );
}
}
}
}
GIVEN( "Empty C-array and std::array" )
{
const csc240::Point p{ 5.0, 5.0 };
const int index = 0;
RawArray rawArray { 1 };
std::array<csc240::Point,1> stdArray;
WHEN( "Setting values using sub-script operator" )
{
rawArray[index] = p;
stdArray[index] = p;
THEN( "Values are same" )
{
REQUIRE( rawArray[index] == stdArray[index] );
}
}
}
GIVEN( "std::array instance" )
{
ModernArray<0> stdArray;
WHEN( "Attempting to access value out of bounds" )
{
THEN( "Throws exception" )
{
REQUIRE_THROWS( stdArray.at( 0 ) );
}
}
}
GIVEN( "An input stream of numbers" )
{
std::stringstream ss;
ss << 1 << std::endl << 2 << std::endl << 3;
WHEN( "Initialising C-array or std::array" )
{
int ra[3];
std::array<int,3> sa;
THEN( "Work similarly" )
{
std::copy( std::istream_iterator<int>( ss ),
std::istream_iterator<int>(), ra );
ss.clear();
ss.seekg( 0, std::ios::beg );
std::copy( std::istream_iterator<int>( ss ),
std::istream_iterator<int>(), std::begin( sa ) );
REQUIRE( sa.size() == csc240::arraysize( ra ) );
for ( uint8_t i = 0; i < sa.size(); ++i )
{
REQUIRE( ra[i] == sa[i] );
}
}
}
}
}
- Compile the project using
F7
- Run the executable generated by the project using
F5
. Note:- You can use
F5
to compile and run the project in one step. - Use
CTRL+F5
to run the project without closing the DOS Command window. This is useful to see the output from the unit test suite.
- You can use
Using std::vector
We will use a std::vector
to replace a raw C-style array.
- Create a C++ source file named
vector
to the project. We will build a BDD style test that illustrates that we can use astd::vector
in exactly the same way as a C-array. The scenario for the test will be that we can replace any C-style array with astd::vector
instance. We will then test for a few common use cases such as initialising the array/vector, iterating over values in the array/vector, updating values in the array/vector etc. Note: We useemplace_back
instead ofpush_back
to add values to the vector. This avoids unnecessary copies of values being added to the vector.
#include "catch.hpp"
#include "Point.h"
#include <vector>
using PointArray = csc240::Point[];
using PointVector = std::vector<csc240::Point>;
SCENARIO( "Raw C-style arrays may be replaced with std::vector" )
{
GIVEN( "A raw C array of Point instances" )
{
const PointArray rawArray = VALUES;
const auto n = std::extent< decltype( rawArray ) >::value;
REQUIRE( n == csc240::arraysize( rawArray ) );
WHEN( "Represented as a std::vector" )
{
const PointVector stdVector VALUES;
THEN( "Size should be same" )
{
REQUIRE( n == stdVector.size() );
}
THEN( "Sub-script operator should yield similar results" )
{
for ( int i = 0; i < n; ++i )
{
REQUIRE( rawArray[i] == stdVector[i] );
}
}
THEN( "Pointer arithmetic works identically" )
{
for ( int i = 0; i < n; ++i )
{
REQUIRE( *( rawArray + i ) == *( stdVector.data() + i ) );
REQUIRE( *( rawArray + i ) == *( &stdVector[0] + i ) );
}
}
}
}
GIVEN( "Empty C-array and std::vector" )
{
const int index = 0;
PointArray rawArray{ 1 };
PointVector stdVector { 1 };
const csc240::Point p{ 5.0, 5.0 };
WHEN( "Setting values using sub-script operator" )
{
rawArray[index] = p;
stdVector[index] = p;
THEN( "Values are same" )
{
REQUIRE( rawArray[index] == stdVector[index] );
}
}
}
GIVEN( "std::vector instance" )
{
PointVector stdVector;
WHEN( "Attempting to access value out of bounds" )
{
THEN( "Throws exception" )
{
REQUIRE_THROWS( stdVector.at( 0 ) );
}
}
WHEN( "Adding items to end" )
{
THEN( "Vector grows" )
{
stdVector.emplace_back( csc240::Point() );
REQUIRE_FALSE( stdVector.empty() );
}
}
}
GIVEN( "An input stream of numbers" )
{
std::stringstream ss;
ss << 1 << std::endl << 2 << std::endl << 3;
WHEN( "Initialising C-array or std::vector" )
{
int ra[3];
std::vector<int> sa;
sa.reserve( 3 );
THEN( "Work similarly" )
{
std::copy( std::istream_iterator<int>( ss ),
std::istream_iterator<int>(), ra );
ss.clear();
ss.seekg( 0, std::ios::beg );
std::copy( std::istream_iterator<int>( ss ),
std::istream_iterator<int>(), std::back_inserter( sa ) );
REQUIRE( sa.size() == csc240::arraysize( ra ) );
for ( uint8_t i = 0; i < sa.size(); ++i )
{
REQUIRE( ra[i] == sa[i] );
}
}
}
}
}
- Compile and run the project using
CTRL+F5
Parallel Arrays
We will use a few standard library containers to replace parallel arrays.
- Add a new header file named
Month
to the project. We will define a simple structure that represents a month of year. We will use that while exercising parallel arrays and their higher level replacements.
#pragma once
#include <string>
#include <ostream>
namespace csc240
{
struct Month { uint8_t days; std::string name; };
inline bool operator == ( const Month& left, const Month& right )
{
return ( left.name == right.name ) && ( left.days == right.days );
}
inline std::ostream& operator << ( std::ostream& stream, const Month& month )
{
stream << "Month - name: " << month.name << ", days: " << month.days << std::endl;
}
}
- Create a C++ source file named
parallel
to the project. We will build a BDD style test that illustrates that we can use a couple of different data structures to replace parallel arrays. We will use the same name of month and number of days in a month example used in the text book. We will use astd::array<std::tuple,12>
data structure to replace the parallel array in the first test scenario. In the second scenario, we will use astd::map<uint8_t,csc240::Month>
data structure to replace the parallel array. You will notice that in both options, we have unified access to a single index, rather than assuming that two different data structures share the same index.
#include "catch.hpp"
#include "Month.h"
#include <array>
#include <map>
#include <tuple>
namespace csc240
{
const uint8_t MONTHS = 12;
const std::string names[] =
{
"January", "February", "March", "April", "May", "June",
"July", "August", "September", "October", "November", "December"
};
const uint8_t days[] =
{
31, 28, 31, 30, 31, 30,
31, 31, 30, 31, 30, 31
};
}
SCENARIO( "A parallel array may be represented using array of std::tuple" )
{
GIVEN( "Given a parallel array for month name and days" )
{
WHEN( "Represented as an array of tuples" )
{
using csc240::MONTHS;
using csc240::names;
using csc240::days;
using Tuple = std::tuple<std::string, uint8_t>;
using Array = std::array<Tuple, MONTHS>;
Array array;
for ( uint8_t i = 0; i < MONTHS; ++i )
{
array[i] = std::make_tuple( names[i], days[i] );
}
THEN( "Access by month number is equivalent" )
{
for ( uint8_t i = 0; i < MONTHS; ++i )
{
REQUIRE( names[i] == std::get<0>( array[i] ) );
REQUIRE( days[i] == std::get<1>( array[i] ) );
}
}
}
}
}
SCENARIO( "A parallel array may be represented using std::map of csc240::Month" )
{
GIVEN( "Given a parallel array for month name and days" )
{
WHEN( "Represented as a map of Month objects" )
{
using csc240::MONTHS;
using csc240::Month;
using csc240::names;
using csc240::days;
using Map = std::map<uint8_t, Month>;
Map map;
for ( uint8_t i = 0; i < MONTHS; ++i )
{
map.emplace( i, Month{ days[i], names[i] } );
}
THEN( "Access by month number is equivalent" )
{
for ( uint8_t i = 0; i < MONTHS; ++i )
{
REQUIRE( names[i] == map[i].name );
REQUIRE( days[i] == map[i].days );
}
}
}
}
}
- Compile and run the project using
CTRL+F5