5

While refactoring some code for performance the other day, I needed an answer to creating member variables that are lazy initialized, but that also provides a convenient, though optional, non-lambda interface for non c++11 compilers.

Here's the typical pattern for lazy instantiation that I want to abstract:

if( !bInitialized )
{
    value = doInitialization();
    bInitialized = true;
}
return value;

For my use, I'd like some flexibility:

  • allow explicit initialization, like the example above
  • provide implicit access to the lazy as if it were the underlying datatype
  • handle uninitialized access (throws), in case I screw up on explicit initialization (e.g., forget to assign a value)
  • also support real lazy initialization via a function, functor and/or lambda
  • allow manual initialization via pointer to the contained value (e.g., when calling Win32 API's)
  • allow reassignment of the value; treat the lazy as the underlying datatype in most cases.

I have code that I'm going to post as an answer, but would be interested in different approaches. Your answer need not satisfy all these requirements; simpler may be better for some use cases...

James Hugard
  • 3,232
  • 1
  • 25
  • 36

1 Answers1

5

Here's my solution, including a unit test suite built on the Microsoft Native Unit Test Library.

It handles the requirements of the OP - a single Lazy class that provides:

  • implicit "on first use" initialization via a function callback, functor, or lambda
  • or, explicit initialization via assignment
  • or, explicit initialization via a pointer to the inner data structure
  • it will throw an exception if an attempt is made to access an uninitialized lazy; e.g., to indicate you forgot to explicitly initialize the lazy and there is no implicit initializer

Plus,

  • it can work with data types that have no default constructor
  • and also handles lazy resource management via optional deinitialization (close) callback

First, an example of usage:

class MyClass
{
    Lazy<int> m_test;
    Lazy<int> m_testViaInitializer;
    Lazy<int> m_testViaInitializationOfPointer;

public:
    MyClass::MyClass()
        : m_testViaInitializer( intFactory )
    {
    }

    int MyClass::lazy_ImplicitInitialization()
    {
        return m_testViaInitializer;
    }

    int MyClass::lazy_ExplicitInitialization()
    {
        if( !m_test.isInitialized() )
        {
            m_test = 42;
        }
        return m_test;
    }

    int MyClass::lazy_InitializationViaPointer()
    {
        if( !m_test.isInitialized() )
        {
            intFactoryViaPointer( & m_testViaInitializationOfPointer );
            m_testViaInitializationOfPointer.forceInitialized();
        }
        return m_testViaInitializationOfPointer;
    }

    Lazy<FILE*> MyClass::lazy_ResourceManagement()
    {
        Lazy<FILE*> lazyFile(
            /*open*/  []() { return fopen("test.txt", "w"); },
            /*close*/ [](FILE*& h) { fclose(h); } );

        return lazyFile;
    }

private:
    static int intFactory()
    {
        return 42;
    }

    static void intFactoryViaPointer( int * v )
    {
        *v = 42;
    }

};

And, here is the code. This version uses the stdc++11 library, but can easily be converted to use boost.

Lazy.hpp

#pragma once

#include <functional>
#include <stdexcept>

// Exception thrown on attempt to access an uninitialized Lazy
struct uninitialized_lazy_exception : public std::runtime_error
{
    uninitialized_lazy_exception()
        :std::runtime_error( "uninitialized lazy value" )
    {}
};

template<typename T>
struct Lazy
{
    // Default constructor
    Lazy()
        :m_bInitialized(false)
        ,m_initializer(DefaultInitializer)
        ,m_deinitializer(DefaultDeinitializer)
    {
    }

    // Construct with initializer and optional deinitializer functor
    Lazy( std::function<T(void)> initializer, std::function<void(T&)> deinitializer = DefaultDeinitializer )
        :m_bInitialized(false)
        ,m_initializer(initializer)
        ,m_deinitializer(deinitializer)
    {
    }

    // Copy constructor
    Lazy( const Lazy& o )
        :m_bInitialized(false)
        ,m_initializer(o.m_initializer)
        ,m_deinitializer(o.m_deinitializer)
    {
        if( o.m_bInitialized )
            construct( *o.valuePtr() );
    }

    // Assign from Lazy<T>
    Lazy& operator=( const Lazy<T>& o )
    {
        destroy();
        m_initializer   = o.m_initializer;
        m_deinitializer = o.m_deinitializer;
        if( o.m_bInitialized )
            construct(*o.valuePtr());
        return *this;
    }

    // Construct from T
    Lazy( const T& v )
        :m_bInitialized(false)
        ,m_initializer(DefaultInitializer)
        ,m_deinitializer(DefaultDeinitializer)
    {
        construct(v);
    }

    // Assign from T
    T& operator=(const T& value )
    {
        construct(value);
        return *valuePtr();
    }

    // Destruct and deinitialize
    ~Lazy()
    {
        destroy();
    }

    // Answer true if initialized, either implicitly via function or explicitly via assignment
    bool isInitialized() const
    {
        return m_bInitialized;
    }

    // Force initialization, if not already done, and answer with the value
    // Throws exception if not implicitly or explicitly initialized
    T& force() const
    {
        if( !m_bInitialized )
        {
            construct(m_initializer());
        }
        return *valuePtr();
    }

    // Implicitly force initialization and answer with value
    operator T&() const
    {
        return force();
    }

    // Get pointer to storage of T, regardless of initialized state
    T* operator &() const
    {
        return valuePtr();
    }

    // Force initialization state to true, e.g. if value initialized directly via pointer
    void forceInitialized()
    {
        m_bInitialized = true;
    }

private:
    mutable char            m_value[sizeof(T)];
    mutable bool            m_bInitialized;
    std::function<T(void)>  m_initializer;
    std::function<void(T&)> m_deinitializer;

    // Get pointer to storage of T
    T* valuePtr() const
    {
        return static_cast<T*>( static_cast<void*>( &m_value ) );
    }

    // Call copy constructor for T.  Deinitialize self first, if necessary.
    void construct(const T& value) const
    {
        destroy();
        new (valuePtr()) T(value);
        m_bInitialized = true;
    }

    // If initialized, call deinitializer and then destructor for T
    void destroy() const
    {
        if( m_bInitialized )
        {
            m_deinitializer(*valuePtr());
            valuePtr()->~T();
            m_bInitialized = false;
        }
    }

    // Inititializer if none specified; throw exception on attempt to access uninitialized lazy
    static T DefaultInitializer()
    {
        throw uninitialized_lazy_exception();
    }

    // Deinitialize if none specified; does nothing
    static void DefaultDeinitializer(T&)
    {
    }
};

test_Lazy.cpp

#include "stdafx.h"
#include "CppUnitTest.h"

#include "Lazy.hpp"
#include <memory>
#include <string>

using namespace std;

using namespace Microsoft::VisualStudio::CppUnitTestFramework;

namespace Lazy_Test
{       
    TEST_CLASS(test_Lazy)
    {
    public:
        TEST_METHOD(Lazy_ReturnsValueOnForce)
        {
            const Lazy<int> test( []()
            {
                return 42;
            } );
            Assert::AreEqual( false, test.isInitialized() );
            Assert::AreEqual( 42, test.force() );
            Assert::AreEqual( true, test.isInitialized() );
        }

        TEST_METHOD(Lazy_ManualInitialization)
        {
            Lazy<int> test;
            Assert::AreEqual( false, test.isInitialized() );

            if( !test.isInitialized() )
            {
                test = 42;
            }

            Assert::AreEqual( 42, (int)test );
            Assert::AreEqual( 42, test.force() );
            Assert::AreEqual( true, test.isInitialized() );
        }

        TEST_METHOD(UninitializedLazy_ThrowsExceptionOnForce)
        {
            const Lazy<int> test;
            Assert::AreEqual( false, test.isInitialized() );
            Assert::ExpectException<uninitialized_lazy_exception>( [&test]() { test.force(); } );
        }

        TEST_METHOD(Lazy_ManualInitializationViaPointer)
        {
            Lazy<int> test;
            Assert::AreEqual( false, test.isInitialized() );

            if( !test.isInitialized() )
            {
                int* pTest = &test;
                *pTest = 42;
                test.forceInitialized();
            }

            Assert::AreEqual( true, test.isInitialized() );
            Assert::AreEqual( 42, (int)test );
            Assert::AreEqual( 42, test.force() );
        }

        TEST_METHOD(Lazy_DoesResourceDeinitialization)
        {
            typedef unsigned HANDLE;
            bool bIsOpen = false;   // side-effect for unit testing

            function<HANDLE()> openLambda = [&bIsOpen]() {
                bIsOpen = true;
                return 12345;
            };

            function<void(HANDLE&)> closeLambda = [&bIsOpen](HANDLE& h) {
                bIsOpen = false;
                // e.g.., Close(h);
            };

            {
                Lazy<HANDLE> lazyHandle( openLambda, closeLambda );
                Assert::AreEqual( false, bIsOpen );

                HANDLE h = lazyHandle;
                Assert::AreEqual( true, bIsOpen );
                Assert::AreEqual( (HANDLE)12345, h );
            }
            Assert::AreEqual( false, bIsOpen );
        }

        TEST_METHOD(Lazy_CopiesCorrectly)
        {
            const Lazy<int> a( []() { return 42; } );
            Lazy<int> b;
            Lazy<int> c = (b = a);

            Assert::AreEqual( false, a.isInitialized() );
            Assert::AreEqual( false, b.isInitialized() );
            Assert::AreEqual( false, c.isInitialized() );

            Assert::AreEqual( 42, a.force() );
            Assert::AreEqual( true, a.isInitialized() );
            Assert::AreEqual( false, b.isInitialized() );
            Assert::AreEqual( false, c.isInitialized() );

            Assert::AreEqual( 42, b.force() );
            Assert::AreEqual( false, c.isInitialized() );
            Assert::AreEqual( true, b.isInitialized() );
            Assert::AreEqual( true, a.isInitialized() );

            Assert::AreEqual( 42, c.force() );
            Assert::AreEqual( true, c.isInitialized() );
            Assert::AreEqual( true, b.isInitialized() );
            Assert::AreEqual( true, a.isInitialized() );

        }

        TEST_METHOD(Lazy_HandlesAssignment)
        {
            const Lazy<int> a ([]() { return 42; } );
            Lazy<int> b;

            Assert::AreEqual( false, a.isInitialized() );
            Assert::AreEqual( false, b.isInitialized() );

            Assert::AreEqual( 42, a.force() );
            Assert::AreEqual( true, a.isInitialized() );
            Assert::AreEqual( false, b.isInitialized() );

            b = 43;
            Assert::AreEqual( true, b.isInitialized() );
            Assert::AreEqual( 43, b.force() );

            b = a;
            Assert::AreEqual( true, b.isInitialized() );
            Assert::AreEqual( 42, b.force() );
        }

        TEST_METHOD(Lazy_HandlesNonTrivialClass)
        {
            Lazy<string>    a( []() { return "fee"; } );
            Lazy<string>    b( string("fie") );
            Lazy<string>    c; c = "foe";
            Lazy<string>    d(c);

            Assert::AreEqual( string("fee"), (string)a );
            Assert::AreEqual( string("fie"), (string)b );
            Assert::AreEqual( string("foe"), (string)c );
            Assert::AreEqual( string("foe"), (string)d );

            d = "fum";
            Assert::AreEqual( string("fum"), (string)d );

            Assert::AreEqual( (size_t)3, a.force().length() );
            Assert::AreEqual( (size_t)3, b.force().length() );
            Assert::AreEqual( (size_t)3, c.force().length() );
            Assert::AreEqual( (size_t)3, d.force().length() );

        }

        struct NoDefaultConstructor
        {
            NoDefaultConstructor(bool v) : m_state(v)
            {}
            NoDefaultConstructor(const NoDefaultConstructor& o) : m_state(o.m_state)
            {}
            NoDefaultConstructor& operator=(const NoDefaultConstructor& o)
            {
                m_state=o.m_state;
                return *this;
            }
            bool operator !()
            {
                return !m_state;
            }
        private:
            bool m_state;
        };

        TEST_METHOD(Lazy_HandlesNoDefaultConstructor)
        {
            Lazy<NoDefaultConstructor> v( []() { return NoDefaultConstructor(true); } );
            Assert::IsTrue( !!v.force() );
        }
    };
}
James Hugard
  • 3,232
  • 1
  • 25
  • 36
  • 2
    What are you going to do for types which do not have default constructor? What if this default constructor is of very high cost? Maybe lazy construction is better than lazy assignment to some value? – PiotrNycz Jul 14 '13 at 20:11
  • 1
    @PiotrNycz - Fixed. Handles types with no default constructor and will do lazy construction :-) – James Hugard Jul 15 '13 at 03:09
  • 1
    Try `Lazy`, or any other complicated type with not trivial construction - probably fail in runtime. You cannot apply assignment operator to an object which is not properly constructed, casting is not enough to make an object from raw memory - you need to construct it. That is why placement new exists in C++. – PiotrNycz Jul 15 '13 at 06:36
  • 1
    Another thing - what with destruction? Your object controlled in `Lazy` is not properly destructed. I'd strongly advise to use `boost::optional` - it already implemented many things you simple forget in your lazy class. If you plan to use your class only for POD types - then its name cannot be so general. – PiotrNycz Jul 15 '13 at 06:54
  • @PiotrNycz - Ok done: supports classes with no default constructor, plus added support for deinitialization. Thanks for the tip on placement new/delete... never used those before! – James Hugard Jul 16 '13 at 14:23
  • 1
    Looks better now. One thing in your assignment operator: you first shall deinitialize then replace the deinitializer. BTW, you can also consider to use "copy and swap" idiom - it'd make your assignment operator easier to implement. And you shall use this form of destruction: `valuePtr()->~T();` not this `valuePtr()->T::~T();` - otherwise virtual destructor will not work for you. – PiotrNycz Jul 17 '13 at 07:46
  • @PiotrNycz - Thanks! Fixed the issues, but didn't use copy & swap. – James Hugard Jul 17 '13 at 23:31