Chapter 25: Relationships Between Objects

Introduction

In object-oriented programming, relationships between objects can be categorized into three main types: composition, aggregation, and association. Each type of relationship has specific properties and use cases.

Relationship Types

Composition

Aggregation

Association

std::reference_wrapper

std::reference_wrapper is a class that acts like a reference but also allows assignment and copying, making it compatible with containers like std::vector.

Example

#include <functional> // for std::reference_wrapper
#include <vector>
#include <string>
#include <iostream>

int main()
{
    std::string tom = "Tom";
    std::string berta = "Berta";
    std::vector<std::reference_wrapper<std::string>> names{ tom, berta };

    // Accessing the referenced objects
    std::cout << names[0].get() << '\n'; // prints "Tom"
    std::cout << names[1].get() << '\n'; // prints "Berta"

    return 0;
}

Value Containers vs Reference Containers

Value containers are compositions that store copies of the objects they are holding (and thus are responsible for creating and destroying those copies).

Reference containers are aggregations that store pointers or references to other objects (and thus are not responsible for creation or destruction of those objects).

Example: Value Container

#include <vector>
#include <string>

class Library
{
private:
    std::vector<std::string> books; // value container

public:
    Library(const std::initializer_list<std::string>& bookList)
        : books(bookList) {}

    void addBook(const std::string& book)
    {
        books.push_back(book);
    }

    const std::vector<std::string>& getBooks() const
    {
        return books;
    }
};

Example: Reference Container

#include <vector>
#include <functional> // for std::reference_wrapper
#include <string>

class Library
{
private:
    std::vector<std::reference_wrapper<std::string>> books; // reference container

public:
    Library(const std::initializer_list<std::reference_wrapper<std::string>>& bookList)
        : books(bookList) {}

    void addBook(std::string& book)
    {
        books.push_back(book);
    }

    const std::vector<std::reference_wrapper<std::string>>& getBooks() const
    {
        return books;
    }
};

Using std::initializer_list

std::initializer_list allows classes to be initialized using list initialization.

Example

#include <algorithm> // for std::copy
#include <initializer_list> // for std::initializer_list
#include <iostream>

class IntArray
{
private:
    int* m_data;
    int m_length;

public:
    IntArray(int length)
        : m_length(length)
    {
        m_data = new int[length];
    }

    IntArray(std::initializer_list<int> list)
        : IntArray(static_cast<int>(list.size())) // use delegating constructor to set up initial array
    {
        std::copy(list.begin(), list.end(), m_data); // initialize our array from the list
    }

    ~IntArray()
    {
        delete[] m_data;
    }

    void print() const
    {
        for (int i = 0; i < m_length; ++i)
            std::cout << m_data[i] << ' ';
        std::cout << '\n';
    }
};

int main()
{
    IntArray array{ 1, 2, 3, 4, 5 };
    array.print(); // prints: 1 2 3 4 5
    return 0;
}

Conclusion

Understanding the relationships between objects is crucial for designing robust and maintainable object-oriented systems. Composition, aggregation, and association each have specific properties and use cases that dictate how objects interact and manage their lifetimes. Using containers like std::vector and tools like std::reference_wrapper and std::initializer_list can help manage these relationships effectively.