【问题标题】:How to abstract lazy initialization in C++?如何在 C++ 中抽象延迟初始化?
【发布时间】:2013-07-14 19:21:24
【问题描述】:

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

这是我想抽象的惰性实例化的典型模式:

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

为了我的使用,我想要一些灵活性:

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

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

【问题讨论】:

    标签: c++ boost lazy-loading std lazy-evaluation


    【解决方案1】:

    这是我的解决方案,包括基于 Microsoft 原生单元测试库构建的单元测试套件。

    它处理 OP 的要求 - 单个 Lazy 类提供:

    • 通过函数回调、仿函数或 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。

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

    【讨论】:

    • 对于没有默认构造函数的类型你会怎么做?如果这个默认构造函数的成本非常高怎么办?也许懒惰的构造比懒惰的赋值更好?
    • @PiotrNycz - 已修复。处理没有默认构造函数的类型并且会做惰性构造:-)
    • 尝试Lazy&lt;std::string&gt;,或任何其他复杂的类型,构造不简单——可能在运行时失败。您不能将赋值运算符应用于未正确构造的对象,强制转换不足以从原始内存中创建对象 - 您需要构造它。这就是 C++ 中存在placement new 的原因。
    • 另一件事 - 破坏是什么?您在Lazy 中控制的对象未正确销毁。我强烈建议使用boost::optional - 它已经实现了许多你在懒惰类中容易忘记的东西。如果您打算仅将您的类用于 POD 类型 - 那么它的名称不能如此笼统。
    • 现在看起来好多了。赋值运算符中的一件事:您首先应取消初始化,然后替换取消初始化程序。顺便说一句,您也可以考虑使用“复制和交换”习语 - 它会使您的赋值运算符更容易实现。你应该使用这种形式的破坏:valuePtr()-&gt;~T(); 而不是这个valuePtr()-&gt;T::~T(); - 否则虚拟析构函数对你不起作用。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2020-05-10
    • 1970-01-01
    • 1970-01-01
    • 2011-11-17
    • 1970-01-01
    • 1970-01-01
    • 2020-10-08
    相关资源
    最近更新 更多