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 data structure to replace the parallel array in the first test scenario. In the second scenario, we will use a std::map 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