Sans Pareil Technologies, Inc.

Key To Your Business

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 named Point, 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 a std::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 a std::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 modern using syntax for defining an alias over the old C typedef.
#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.
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 a std::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 a std::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 use emplace_back instead of push_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 a std::array<std::tuple,12> data structure to replace the parallel array in the first test scenario. In the second scenario, we will use a std::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