How to abstract lazy initialization in C++?

2020-07-14 12:07发布

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...

1条回答
兄弟一词,经得起流年.
2楼-- · 2020-07-14 12:58

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() );
        }
    };
}
查看更多
登录 后发表回答