如何在C ++中抽象延迟初始化?

问题描述:

前几天在重构一些代码以提高性能时,我需要一个答案来创建延迟初始化的成员变量,但它也为非c ++ 11编译器提供了一个方便但可选的非lambda接口.

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;

我想要一些灵活性:

  • 允许进行显式初始化,如上面的示例
  • 提供对延迟的隐式访问,就好像它是基础数据类型一样
  • 处理未初始化的访问(抛出),以防万一我搞砸了显式初始化(​​例如,忘记分配值)
  • 还通过函数,函子和/或lambda支持真正的延迟初始化
  • 允许通过指向所包含值的指针进行手动初始化(例如,在调用Win32 API时)
  • 允许重新分配值;在大多数情况下,将懒惰视为基础数据类型.

我有要发布的代码作为答案,但会对其他方法感兴趣.您的答案不必满足所有这些要求;在某些用例中,更简单可能会更好...

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

这是我的解决方案,包括基于Microsoft本机单元测试库构建的单元测试套件.

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

它处理OP的需求-一个单一的Lazy类,它提供:

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

  • 通过函数回调,函子或lambda隐式首次使用"初始化
  • 或通过赋值进行显式初始化
  • 或通过指向内部数据结构的指针进行显式初始化
  • 如果尝试访问未初始化的延迟,则会引发异常;例如,表明您忘记了显式初始化懒惰,并且没有隐式初始化程序

加上

  • 它可以与没有默认构造函数的数据类型一起使用
  • 并通过可选的反初始化(关闭)回调处理惰性资源管理

首先,一个用法示例:

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;
    }

};

并且,这是代码.此版本使用stdc ++ 11库,但可以轻松转换为使用boost.

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() );
        }
    };
}