Search icon CANCEL
Arrow left icon
Explore Products
Best Sellers
New Releases
Books
Videos
Audiobooks
Learning Hub
Conferences
Free Learning
Arrow right icon
Arrow up icon
GO TO TOP
Advanced C++

You're reading from   Advanced C++ Master the technique of confidently writing robust C++ code

Arrow left icon
Product type Paperback
Published in Oct 2019
Publisher
ISBN-13 9781838821135
Length 762 pages
Edition 1st Edition
Languages
Arrow right icon
Authors (5):
Arrow left icon
Olena Lizina Olena Lizina
Author Profile Icon Olena Lizina
Olena Lizina
Rakesh Mane Rakesh Mane
Author Profile Icon Rakesh Mane
Rakesh Mane
Gazihan Alankus Gazihan Alankus
Author Profile Icon Gazihan Alankus
Gazihan Alankus
Brian Price Brian Price
Author Profile Icon Brian Price
Brian Price
Vivek Nagarajan Vivek Nagarajan
Author Profile Icon Vivek Nagarajan
Vivek Nagarajan
+1 more Show less
Arrow right icon
View More author details
Toc

Table of Contents (11) Chapters Close

About the Book 1. Anatomy of Portable C++ Software 2A. No Ducks Allowed – Types and Deduction FREE CHAPTER 2B. No Ducks Allowed – Templates and Deduction 3. No Leaks Allowed - Exceptions and Resources 4. Separation of Concerns - Software Architecture, Functions, and Variadic Templates 5. The Philosophers' Dinner – Threads and Concurrency 6. Streams and I/O 7. Everybody Falls, It's How You Get Back Up – Testing and Debugging 8. Need for Speed – Performance and Optimization 1. Appendix

Chapter 4 - Separation of Concerns - Software Architecture, Functions, Variadic Templates

Activity 1: Implement a multicast event handler

  1. Load the prepared project from the Lesson4/Activity01 folder and configure the Current Builder for the project to be CMake Build (Portable). Build the project, configure the launcher and run the unit tests (which fail the one dummy test). Recommend that the name used for the tests runner is L4delegateTests.
  2. In delegateTests.cpp, replace the failing dummy test with the following test:

    TEST_F(DelegateTest, BasicDelegate)

    {

        Delegate delegate;

        ASSERT_NO_THROW(delegate.Notify(42));

    }

  3. This now fails to build, so we need to add a new method to Delegate. As this will evolve into a template, we will do all of this development in the header file. In delegate.hpp, and the following definition:

    class Delegate

    {

    public:

        Delegate() = default;

        void Notify(int value) const

        {

        }

    };

    The test now runs and passes.

  4. Add the following line to the existing test:

    ASSERT_NO_THROW(delegate(22));

  5. Again, the build fails, so we update the Delegate definition as follows (we could have had Notify call operator(), but this is easier to read):

    void operator()(int value)

    {

        Notify(value);

    }

    The test again runs and passes.

  6. Before we add the next test, we are going to add some infrastructure to help us develop our tests. The easiest thing to do with handlers is have them write to std::cout, and to be able to verify that they were called, we need to capture the output. To do this, re-route the standard output stream to a different buffer by changing the DelegateTest class as follows:

    class DelegateTest : public ::testing::Test

    {

    public:

        void SetUp() override;

        void TearDown() override;

        std::stringstream m_buffer;

        // Save cout's buffer here

        std::streambuf *m_savedBuf{};

    };

    void DelegateTest::SetUp()

    {

        // Save the cout buffer

        m_savedBuf = std::cout.rdbuf();

        // Redirect cout to our buffer

        std::cout.rdbuf(m_buffer.rdbuf());

    }

    void DelegateTest::TearDown()

    {

        // Restore cout buffer to original

        std::cout.rdbuf(m_savedBuf);

    }

  7. Also add the include statements for <iostream>, <sstream> and <string> to the top of the file.
  8. With this support framework in place, add the following test:

    TEST_F(DelegateTest, SingleCallback)

    {

        Delegate delegate;

        delegate += [] (int value) { std::cout << "value = " << value; };

        delegate.Notify(42);

        std::string result = m_buffer.str();

        ASSERT_STREQ("value = 42", result.c_str());

    }

  9. To make the tests build and run again, add the following code in the delegate.h class:

    Delegate& operator+=(const std::function<void(int)>& delegate)

    {

        m_delegate = delegate;

        return *this;

    }

    Along with the following code:

    private:

        std::function<void(int)> m_delegate;

    The tests now build, but our new test fails.

  10. Update the Notify() method to be:

    void Notify(int value) const

    {

        m_delegate(value);

    }

  11. The tests now build and our new test passes, but the original test now fails. The call to the delegate is throwing an exception, so we need to check that the delegate is not empty before calling it. Write the following code to do this:

    void Notify(int value) const

    {

        if(m_delegate)

            m_delegate(value);

    }

    All the tests now run and pass.

  12. We now need to add multicast support to the Delegate class. Add the new test:

    TEST_F(DelegateTest, DualCallbacks)

    {

        Delegate delegate;

        delegate += [] (int value) { std::cout << "1: = " << value << "\n"; };

        delegate += [] (int value) { std::cout << "2: = " << value << "\n"; };

        delegate.Notify(12);

        std::string result = m_buffer.str();

        ASSERT_STREQ("1: = 12\n2: = 12\n", result.c_str());

    }

  13. Of course, this test now fails because the operator+=() only assigns to the member variable. We need to add a list to store our delegates. We choose vector so we can add to the end of the list as we want to call the delegates in the order that they are added. Add #include <vector> to the top of delegate.hpp and update Delegate replace m_delegate with m_delegates vector of the callbacks:

    class Delegate

    {

    public:

        Delegate() = default;

        Delegate& operator+=(const std::function<void(int)>& delegate)

        {

            m_delegates.push_back(delegate);

            return *this;

        }

        void Notify(int value) const

        {

            for(auto& delegate : m_delegates)

            {

                delegate(value);

            }

        }

        void operator()(int value)

        {

            Notify(value);

        }

    private:

        std::vector<std::function<void(int)>> m_delegates;

    };

    The tests all run and pass again.

  14. We now have the basic multicast delegate class implemented. We now need to convert it to a template- based class. Update the existing tests, by changing all of the declarations of Delegate to Delegate<int> in the three tests.
  15. Now update the Delegate class by adding template<class Arg> before the class to convert it to a template, and substituting the four occurrences of int with Arg:

    template<class Arg>

    class Delegate

    {

    public:

        Delegate() = default;

        Delegate& operator+=(const std::function<void(Arg)>& delegate)

        {

            m_delegates.push_back(delegate);

            return *this;

        }

        void Notify(Arg value) const

        {

            for(auto& delegate : m_delegates)

            {

                delegate(value);

            }

        }

        void operator()(Arg value)

        {

            Notify(value);

        }

    private:

        std::vector<std::function<void(Arg)>> m_delegates;

    };

  16. All the tests now run and pass as previously, so it stills works for int arguments for the handlers.
  17. Add the following test and re-run the tests to confirm that the template conversion is correct:

    TEST_F(DelegateTest, DualCallbacksString)

    {

        Delegate<std::string&> delegate;

        delegate += [] (std::string value) { std::cout << "1: = " << value << "\n"; };

        delegate += [] (std::string value) { std::cout << "2: = " << value << "\n"; };

        std::string hi{"hi"};

        delegate.Notify(hi);

        std::string result = m_buffer.str();

        ASSERT_STREQ("1: = hi\n2: = hi\n", result.c_str());

    }

  18. Now it operates as a template that takes one argument. We need to convert it into a variadic template that takes zero or more arguments. Using the information from the last topic, update the template to the following:

    template<typename... ArgTypes>

    class Delegate

    {

    public:

        Delegate() = default;

        Delegate& operator+=(const std::function<void(ArgTypes...)>& delegate)

        {

            m_delegates.push_back(delegate);

            return *this;

        }

        void Notify(ArgTypes&&... args) const

        {

            for(auto& delegate : m_delegates)

            {

                delegate(std::forward<ArgTypes>(args)...);

            }

        }

        void operator()(ArgTypes&&... args)

        {

            Notify(std::forward<ArgTypes>(args)...);

        }

    private:

        std::vector<std::function<void(ArgTypes...)>> m_delegates;

    };

    The tests should still run and pass.

  19. Add two more tests – zero argument test, and a mutliple argument test:

    TEST_F(DelegateTest, DualCallbacksNoArgs)

    {

        Delegate delegate;

        delegate += [] () { std::cout << "CB1\n"; };

        delegate += [] () { std::cout << "CB2\n"; };

        delegate.Notify();

        std::string result = m_buffer.str();

        ASSERT_STREQ("CB1\nCB2\n", result.c_str());

    }

    TEST_F(DelegateTest, DualCallbacksStringAndInt)

    {

        Delegate<std::string&, int> delegate;

        delegate += [] (std::string& value, int i) {

                std::cout << "1: = " << value << "," << i << "\n"; };

        delegate += [] (std::string& value, int i) {

            std::cout << "2: = " << value << "," << i << "\n"; };

        std::string hi{"hi"};

        delegate.Notify(hi, 52);

        std::string result = m_buffer.str();

        ASSERT_STREQ("1: = hi,52\n2: = hi,52\n", result.c_str());

    }

    All the tests run and pass showing that we have now implemented the desired Delegate class.

  20. Now, change the Run configuration to execute the program L4delegate. Open the main.cpp file in the editor and change the definition at the top of the file to the following and run the program:

    #define ACTIVITY_STEP 27

    We get the following output:

Figure 4.35: Output from the successful implementation of Delegate
Figure 4.35: Output from the successful implementation of Delegate

In this activity, we started by implementing a class that provides the basic single delegate functionality and then added multicast capability. With that implemented, and unit tests in place, we were quickly able to convert to a template with one argument and then to a variadic template version. Depending on the functionality that you are developing, the approach of the specific implementation transitioning to a general form and then to an even more general form is the correct one. Development of variadic templates is not always obvious.

lock icon The rest of the chapter is locked
Register for a free Packt account to unlock a world of extra content!
A free Packt account unlocks extra newsletters, articles, discounted offers, and much more. Start advancing your knowledge today.
Unlock this book and the full library FREE for 7 days
Get unlimited access to 7000+ expert-authored eBooks and videos courses covering every tech area you can think of
Renews at $19.99/month. Cancel anytime