访问单例时避免未定义行为的技术

本文讨论了在现代c ++中访问单例时避免未定义行为的原因和方法。 提供了单线程代码的示例。 没有什么是编译器专用的,全部符合标准。

引言


首先,建议您阅读有关Habré的有关singleton的其他文章:

Singleton模式的三个时代
单例和公共实例
打破单一责任原则的3种方法
单例-模式还是反模式?
使用单例模式

最后,一篇涉及相同主题但漏了的文章(如果只是因为没有考虑到缺点和局限性):
个性化对象(即对象
单例和对象寿命

下一个:

  • 不是关于单例的建筑属性的文章;
  • 不是一篇文章“如何从一个可怕的和可怕的单身人士中做出一个白色而蓬松的单身人士”;
  • 不是一个单身人士的竞选活动;
  • 不是对单身人士的十字军东征;
  • 不是一个幸福的结局。

本文是关于在现代C ++中使用单例的一个非常重要但仍然是技术方面的信息。 本文主要关注单例破坏的时刻, 在大多数资料中,销毁问题鲜为人知。 通常,重点放在创建单例的那一刻,至于破坏,充其量是说“以相反顺序破坏”。

我将请您在评论中关注文章的内容,尤其是不要安排“单例模式与单例-反模式”的大节。

所以走吧

标准怎么说


引文来自C ++ 14最终草案N3936 可用的C ++ 17草稿未标记为“最终”。
我完整地介绍了最重要的部分。 重要的地方由我突出显示。

3.6.3终止[basic.start.term]

1.由于从main返回的结果以及调用std :: exit(18.5)的结果,调用了具有静态存储持续时间的初始化对象(即,生命周期(3.8)已经开始的对象)的析构函数(12.4)。 从该线程的初始函数返回的结果以及该线程调用std :: exit的结果,将调用给定线程中具有线程存储持续时间的初始化对象的析构函数。 在启动具有静态存储持续时间的任何对象的析构函数之前,对在该线程内具有线程存储持续时间的所有初始化对象的析构函数的完成顺序进行排序。 如果对具有线程存储持续时间的对象的构造函数的完成或动态初始化的排序要比另一个对象的顺序提前,则在第二个对象的析构函数的初始化之前对第二个对象的析构函数的完成进行排序。 如果具有静态存储持续时间的对象的构造函数完成或动态初始化在另一个对象之前进行排序,则第二个对象的析构函数的完成在第一个对象的析构函数的初始化之前进行排序。 [注:此定义允许同时销毁。 –尾注]如果静态初始化对象,则该对象将以与动态初始化对象相同的顺序销毁。 对于数组或类类型的对象,该对象的所有子对象都必须先销毁,然后再销毁在构造子对象期间初始化了静态存储持续时间的任何块范围对象。 如果通过异常退出具有静态或线程存储持续时间的对象的销毁,则将调用std :: terminate(15.5.1)。

2. 如果函数包含已被破坏的静态或线程存储持续时间的块范围对象,并且在销毁具有静态或线程存储持续时间的对象的过程中调用了该函数,则如果控制流通过,则程序将具有未定义的行为通过先前破坏的块镜对象的定义。 同样,如果块作用域对象在销毁后被间接使用(即通过指针),则该行为也是不确定的。

3.如果在调用std :: atexit之前按顺序对完成了具有静态存储持续时间的对象的初始化进行了排序(请参见“ cstdlib”,18.5),则在调用之前对传递给std :: atexit的函数的调用进行了排序。给对象的析构函数。 如果在具有静态存储持续时间的对象初始化完成之前对std :: atexit的调用进行了排序,则在传递给std :: atexit的函数的调用之前,对对象的析构函数的调用进行了排序。 如果对std :: atexit的调用在另一个对std :: atexit的调用之前被排序,则传递给第二个std :: atexit调用的函数的调用在传递给第一个std :: atexit调用的函数之前被排序。 。

4.如果在信号处理程序(18.10)中不允许使用标准库对象或函数,而该标准库对象或函数在(1.10)完成具有静态存储持续时间的对象销毁并执行std :: atexit注册函数(18.5)之前没有发生),则该程序具有未定义的行为。 [注意:如果使用的对象的静态存储期在销毁对象之前没有发生,则程序具有未定义的行为。 在调用std :: exit或main的退出之前终止每个线程就可以满足这些要求,但这不是必需的。 这些要求允许线程管理器作为静态存储持续时间对象。 —尾注]

5.调用在“ cstdlib”中声明的函数std :: abort()会终止程序,而不执行任何析构函数,也不会调用传递给std :: atexit()或std :: at_quick_exit()的函数。
释义:

  • 具有线程存储持续时间的对象的销毁以其创建的相反顺序进行;
  • 严格地说,销毁具有静态存储持续时间的对象,并以创建和注册该对象的相反顺序调用向std :: atexit注册的函数。
  • 尝试使用线程存储持续时间或静态存储持续时间访问已破坏的对象包含未定义的行为。 没有提供此类对象的重新初始化。

注意:标准中的全局变量称为“具有静态存储持续时间的非局部变量”。 结果,事实证明,所有全局变量,所有单调(局部静态变量)以及对std :: atexit的所有调用在创建/注册时都落入单个LIFO队列中。

对文章有用的信息也包含在3.6.2节中,非局部变量的初始化[basic.start.init] 。 我只介绍最重要的:
具有静态存储持续时间的非局部变量的动态初始化是有序的或无序的。 单个翻译单元中定义有序初始化的变量应按照其在翻译单元中定义的顺序进行初始化。
解释(考虑到本节的全文):一个翻译单元内的全局变量按声明顺序初始化。

代码中将包含什么


本文提供的所有代码示例均在github上发布。

该代码包括三层,就像是由不同的人编写的一样:

  • 单身人士
  • 实用程序(使用单例的类);
  • 用户(全局变量和main)。

Singleton和该实用程序就像第三方库,用户就是用户。
实用程序层旨在将用户层与单例层隔离。 在示例中,用户有机会访问单例,但是我们将采取行动,好像不可能。

用户首先正确地完成所有操作,然后轻弹一下手腕,一切都会中断。 首先,我们尝试在实用程序层中对其进行修复,如果无法解决,则在单例层中进行修复。

在代码中,我们将不断地沿着边缘行进-现在在光亮的一面,然后在黑暗的一面。 为了更容易切换到黑暗的一面,选择了最困难的情况-从实用程序析构函数访问单例。

为什么从析构函数调用的情况最困难? 因为可以在最小化应用程序的过程中调用实用程序析构函数,所以“是否已销毁单例”问题变得很重要。

这个案子是合成的。 实际上,不需要从析构函数调用单例。 即使需要。 例如,记录销毁对象。

使用了三类单例:

  • SingletonClassic-没有智能指针。 实际上,它不是直接很经典,而是绝对是考虑的三者中最经典的一个。
  • SingletonShared-使用std :: shared_ptr;
  • SingletonWeak-具有std :: weak_ptr。

所有单调都是模板。 template参数用于从中继承。 在大多数示例中,它们是由Payload类参数化的,该类提供了一个公共函数,用于将数据添加到std :: set。

在大多数示例中,实用程序析构函数试图在此处填写一百个值。 单例构造函数,单例析构函数和instance()也使用诊断输出到控制台。

为什么这么辛苦? 为了更容易理解我们处于黑暗面。 呼吁被破坏的单身人士是未定义的行为,但可能无法以任何方式从外部体现出来。 将值填充到销毁的std :: set中当然也不能保证外部表现,但是没有更可靠的方法(实际上,在Linux下的GCC中,使用经典单例的不正确示例中,销毁的std :: set被成功填充,而在MSVS下Windows-挂起)。 对于未定义的行为,可能不会输出到控制台。 因此,在正确的示例中,我们希望在析构函数之后无法访问instance(),并且不存在崩溃和挂起的情况,对于不正确的情况,我们希望存在这种上诉,崩溃或挂起的情况,或者同时存在任意组合或任何其他方式。

经典单身


有效载荷
#pragma once #include <set> class Payload { public: Payload() = default; ~Payload() = default; Payload(const Payload &) = delete; Payload(Payload &&) = delete; Payload& operator=(const Payload &) = delete; Payload& operator=(Payload &&) = delete; void add(int value) { m_data.emplace(value); } private: std::set<int> m_data; }; 


单例经典
 #pragma once #include <iostream> template<typename T> class SingletonClassic : public T { public: ~SingletonClassic() { std::cout << "~SingletonClassic()" << std::endl; } SingletonClassic(const SingletonClassic &) = delete; SingletonClassic(SingletonClassic &&) = delete; SingletonClassic& operator=(const SingletonClassic &) = delete; SingletonClassic& operator=(SingletonClassic &&) = delete; static SingletonClassic& instance() { std::cout << "instance()" << std::endl; static SingletonClassic inst; return inst; } private: SingletonClassic() { std::cout << "SingletonClassic()" << std::endl; } }; 


SingletonClassic示例1


Classic_Example1_correct.cpp
 #include "SingletonClassic.h" #include "Payload.h" #include <memory> class ClassicSingleThreadedUtility { public: ClassicSingleThreadedUtility() { // To ensure that singleton will be constucted before utility SingletonClassic<Payload>::instance(); } ~ClassicSingleThreadedUtility() { auto &instance = SingletonClassic<Payload>::instance(); for ( int i = 0; i < 100; ++i ) instance.add(i); } }; // 1. Create an empty unique_ptr // 2. Create singleton (because of modified ClassicSingleThreadedUtility c-tor) // 3. Create utility std::unique_ptr<ClassicSingleThreadedUtility> emptyUnique; auto utilityUnique = std::make_unique<ClassicSingleThreadedUtility>(); // This guarantee destruction in order: // - utilityUnique; // - singleton; // - emptyUnique. // This order is correct int main() { return 0; } 


控制台输出
实例()
SingletonClassic()
实例()
〜SingletonClassic()

该实用程序在构造函数中调用单例,以确保在创建该实用程序之前已创建单例。

用户创建两个std :: unique_ptr:一个为空,第二个包含实用程序。

创建顺序:

-空的std :: unique_ptr。
-单身人士;
-实用程序。

相应地,销毁顺序为:

-实用程序;
-单身人士;
-空的std :: unique_ptr。

从实用程序析构函数到单例的调用是正确的。

SingletonClassic示例2


一切都一样,但是用户接过它,用一行就毁了一切。

Classic_Example2_incorrect.cpp
 #include "SingletonClassic.h" #include "Payload.h" #include <memory> class ClassicSingleThreadedUtility { public: ClassicSingleThreadedUtility() { // To ensure that singleton will be constucted before utility SingletonClassic<Payload>::instance(); } ~ClassicSingleThreadedUtility() { auto &instance = SingletonClassic<Payload>::instance(); for ( int i = 0; i < 100; ++i ) instance.add(i); } }; // 1. Create an empty unique_ptr // 2. Create singleton (because of modified ClassicSingleThreadedUtility c-tor) // 3. Create utility std::unique_ptr<ClassicSingleThreadedUtility> emptyUnique; auto utilityUnique = std::make_unique<ClassicSingleThreadedUtility>(); // This guarantee destruction in order: // - utilityUnique; // - singleton; // - emptyUnique. // This order seems to be correct ... int main() { // ... but user swaps unique_ptrs emptyUnique.swap(utilityUnique); // Guaranteed destruction order is still the same: // - utilityUnique; // - singleton; // - emptyUnique, // but now utilityUnique is empty, and emptyUnique is filled, // so destruction order is incorrect return 0; } 


控制台输出
实例()
SingletonClassic()
〜SingletonClassic()
实例()

保留创建和销毁的顺序。 似乎一切仍然静止。 但是没有 通过调用emptyUnique.swap(utilityUnique),用户提交了未定义的行为。

用户为什么要做这些愚蠢的事情? 因为他对库的内部结构一无所知,这为他提供了一个单例和实用程序。

如果您知道图书馆的内部结构? ...然后,无论如何,使用真实代码很容易参与其中。 而且你不得不痛苦地背囊走,因为 要了解到底发生了什么并不容易。

为什么不要求正确使用库? 好吧,这里有各种各样的码头,例子……而为什么不建造一个不那么容易破坏的图书馆呢?

SingletonClassic示例3


在准备几天的文章的过程中,我相信不可能从实用程序层的上一个示例中消除不确定的行为,并且该解决方案仅在单例层中可用。 但是随着时间的流逝,仍然出现了解决方案。

在打开扰流器的代码和说明之前,我建议读者尝试自己找到一种解决方法(仅在实用程序层中!)。 我不排除有更好的解决方案。

Classic_Example3_correct.cpp
 #include "SingletonClassic.h" #include "Payload.h" #include <memory> #include <iostream> class ClassicSingleThreadedUtility { public: ClassicSingleThreadedUtility() { thread_local auto flag_strong = std::make_shared<char>(0); m_flag_weak = flag_strong; SingletonClassic<Payload>::instance(); } ~ClassicSingleThreadedUtility() { if ( !m_flag_weak.expired() ) { auto &instance = SingletonClassic<Payload>::instance(); for ( int i = 0; i < 100; ++i ) instance.add(i); } } private: std::weak_ptr<char> m_flag_weak; }; // 1. Create an empty unique_ptr // 2. Create singleton (because of modified ClassicSingleThreadedUtility c-tor) // 3. Create utility std::unique_ptr<ClassicSingleThreadedUtility> emptyUnique; auto utilityUnique = std::make_unique<ClassicSingleThreadedUtility>(); // This guarantee destruction in order: // - utilityUnique; // - singleton; // - emptyUnique. // This order seems to be correct ... int main() { // ... but user swaps unique_ptrs emptyUnique.swap(utilityUnique); { // To demonstrate normal processing before application ends auto utility = ClassicSingleThreadedUtility(); } // Guaranteed destruction order is still the same: // - utilityUnique; // - singleton; // - emptyUnique, // but now utilityUnique is empty, and emptyUnique is filled, // so destruction order is incorrect ... // ... but utility uses a variable with thread storage duration to detect thread termination. return 0; } 


控制台输出
实例()
SingletonClassic()
实例()
实例()
〜SingletonClassic()

解说
仅在最小化应用程序时才会出现此问题。 通过教实用程序识别何时最小化应用程序,可以消除未定义的行为。 为此,我们使用了std :: shared_ptr类型的flag_strong变量,该变量具有线程存储持续时间限定符(请参见上文文章的标准摘录)-就像一个静态变量,但是仅当当前线程在任何静态变量被销毁之前(包括销毁之前)结束时才被销毁单身人士。 flag_strong变量是整个流的一个,该实用程序的每个实例都存储其弱副本。

从狭义上讲,该解决方案可以称为hack,因为 它是间接的和非显而易见的。 此外,它警告得太早,有时(在多线程应用程序中)通常警告为false。 但是从广义上讲,这不是hack,而是完全由标准属性(包括缺点和优点)定义的解决方案。

单身人士


让我们继续基于std :: shared_ptr的修改后的单例。

单例共享
 #pragma once #include <memory> #include <iostream> template<typename T> class SingletonShared : public T { public: ~SingletonShared() { std::cout << "~SingletonShared()" << std::endl; } SingletonShared(const SingletonShared &) = delete; SingletonShared(SingletonShared &&) = delete; SingletonShared& operator=(const SingletonShared &) = delete; SingletonShared& operator=(SingletonShared &&) = delete; static std::shared_ptr<SingletonShared> instance() { std::cout << "instance()" << std::endl; // "new" and no std::make_shared because of private c-tor static auto inst = std::shared_ptr<SingletonShared>(new SingletonShared); return inst; } private: SingletonShared() { std::cout << "SingletonShared()" << std::endl; } }; 


Ai-ai-ai,新运算符不应在现代代码中使用,而需要std :: make_shared! 单身人士的私有构造函数阻止了这种情况。

哈! 我也有问题! 声明std :: make_shared一个单身朋友! ...并获得反模式PublicMorozov的一种变体:使用相同的std :: make_shared,可以创建架构未提供的单例的其他实例。

SingletonShared示例1和2


完全对应于经典版本的示例1和2。 仅对单例层进行了重大更改,实用程序基本上保持不变。 就像经典单例中的示例一样,example-1是正确的,而example-2显示了未定义的行为。

Shared_Example1_correct.cpp
 #include "SingletonShared.h" #include <Payload.h> #include <memory> class SharedSingleThreadedUtility { public: SharedSingleThreadedUtility() { // To ensure that singleton will be constucted before utility SingletonShared<Payload>::instance(); } ~SharedSingleThreadedUtility() { if ( auto instance = SingletonShared<Payload>::instance() ) for ( int i = 0; i < 100; ++i ) instance->add(i); } }; // 1. Create an empty unique_ptr // 2. Create singleton (because of modified SharedSingleThreadedUtility c-tor) // 3. Create utility std::unique_ptr<SharedSingleThreadedUtility> emptyUnique; auto utilityUnique = std::make_unique<SharedSingleThreadedUtility>(); // This guarantee destruction in order: // - utilityUnique; // - singleton; // - emptyUnique. // This order is correct int main() { return 0; } 


控制台输出
实例()
SingletonShared()
实例()
〜SingletonShared()

Shared_Example2_incorrect.cpp
 #include "SingletonShared.h" #include "Payload.h" #include <memory> class SharedSingleThreadedUtility { public: SharedSingleThreadedUtility() { // To ensure that singleton will be constucted before utility SingletonShared<Payload>::instance(); } ~SharedSingleThreadedUtility() { // Sometimes this check may result as "false" even for destroyed singleton // preventing from visual effects of undefined behaviour ... //if ( auto instance = SingletonShared::instance() ) // for ( int i = 0; i < 100; ++i ) // instance->add(i); // ... so this code will demonstrate UB in colour auto instance = SingletonShared<Payload>::instance(); for ( int i = 0; i < 100; ++i ) instance->add(i); } }; // 1. Create an empty unique_ptr // 2. Create singleton (because of modified SharedSingleThreadedUtility c-tor) // 3. Create utility std::unique_ptr<SharedSingleThreadedUtility> emptyUnique; auto utilityUnique = std::make_unique<SharedSingleThreadedUtility>(); // This guarantee destruction in order: // - utilityUnique; // - singleton; // - emptyUnique. // This order seems to be correct ... int main() { // ... but user swaps unique_ptrs emptyUnique.swap(utilityUnique); // Guaranteed destruction order is the same: // - utilityUnique; // - singleton; // - emptyUnique, // but now utilityUnique is empty, and emptyUnique is filled, // so destruction order is incorrect return 0; } 


控制台输出
实例()
SingletonShared()
〜SingletonShared()
实例()

SingletonShared示例3


现在,我们将比经典的示例3更好地解决此问题。
解决方案是显而易见的:您只需通过将单例返回的std :: shared_ptr副本存储在实用程序中来延长单例的寿命。 并且此解决方案与SingletonShared一起完成,已在开源中广泛复制。

Shared_Example3_correct.cpp
 #include "SingletonShared.h" #include "Payload.h" #include <memory> class SharedSingleThreadedUtility { public: SharedSingleThreadedUtility() // To ensure that singleton will be constucted before utility : m_singleton(SingletonShared<Payload>::instance()) { } ~SharedSingleThreadedUtility() { // Sometimes this check may result as "false" even for destroyed singleton // preventing from visual effects of undefined behaviour ... //if ( m_singleton ) // for ( int i = 0; i < 100; ++i ) // m_singleton->add(i); // ... so this code will allow to demonstrate UB in colour for ( int i = 0; i < 100; ++i ) m_singleton->add(i); } private: // A copy of smart pointer, not a reference std::shared_ptr<SingletonShared<Payload>> m_singleton; }; // 1. Create an empty unique_ptr // 2. Create singleton (because of SharedSingleThreadedUtility c-tor) // 3. Create utility std::unique_ptr<SharedSingleThreadedUtility> emptyUnique; auto utilityUnique = std::make_unique<SharedSingleThreadedUtility>(); int main() { // This guarantee destruction in order: // - utilityUnique; // - singleton; // - emptyUnique. // This order is correct ... // ... but user swaps unique_ptrs emptyUnique.swap(utilityUnique); // Guaranteed destruction order is the same: // - utilityUnique; // - singleton; // - emptyUnique, // but now utilityUnique is empty, and emptyUnique is filled, // so destruction order is incorrect... // ... but utility have made a copy of shared_ptr when it was available, // so it's correct again. return 0; } 


控制台输出
实例()
SingletonShared()
〜SingletonShared()

现在,请注意,问题是: 您是否真的想延长单身人士的寿命?
还是您想摆脱不确定的行为,而选择延长寿命作为一种表面的方式?

用手段替代目标形式的理论上的错误会导致出现死锁的风险(或循环引用-称其为您想要的)。

是的nuuuuuu,这就是您必须尝试的方式! 您将需要花费很长时间,并且您当然不会偶然!

CallbackPayload.h
 #pragma once #include <functional> class CallbackPayload { public: CallbackPayload() = default; ~CallbackPayload() = default; CallbackPayload(const CallbackPayload &) = delete; CallbackPayload(CallbackPayload &&) = delete; CallbackPayload& operator=(const CallbackPayload &) = delete; CallbackPayload& operator=(CallbackPayload &&) = delete; void setCallback(std::function<void()> &&fn) { m_callbackFn = std::move(fn); } private: std::function<void()> m_callbackFn; }; 


SomethingWithVeryImportantDestructor.h
 #pragma once #include <iostream> class SomethingWithVeryImportantDestructor { public: SomethingWithVeryImportantDestructor() { std::cout << "SomethingWithVeryImportantDestructor()" << std::endl; } ~SomethingWithVeryImportantDestructor() { std::cout << "~SomethingWithVeryImportantDestructor()" << std::endl; } SomethingWithVeryImportantDestructor(const SomethingWithVeryImportantDestructor &) = delete; SomethingWithVeryImportantDestructor(SomethingWithVeryImportantDestructor &&) = delete; SomethingWithVeryImportantDestructor& operator=(const SomethingWithVeryImportantDestructor &) = delete; SomethingWithVeryImportantDestructor& operator=(SomethingWithVeryImportantDestructor &&) = delete; }; 


Shared_Example4_incorrect.cpp
 #include "SingletonShared.h" #include "CallbackPayload.h" #include "SomethingWithVeryImportantDestructor.h" class SharedSingleThreadedUtility { public: SharedSingleThreadedUtility() // To ensure that singleton will be constucted before utility : m_singleton(SingletonShared<CallbackPayload>::instance()) { std::cout << "SharedSingleThreadedUtility()" << std::endl; } ~SharedSingleThreadedUtility() { std::cout << "~SharedSingleThreadedUtility()" << std::endl; } void setCallback(std::function<void()> &&fn) { if ( m_singleton ) m_singleton->setCallback(std::move(fn)); } private: // A copy of smart pointer, not a reference std::shared_ptr<SingletonShared<CallbackPayload>> m_singleton; }; int main() { auto utility = std::make_shared<SharedSingleThreadedUtility>(); auto something = std::make_shared<SomethingWithVeryImportantDestructor>(); // lambda with "utility" and "something" captured utility->setCallback( [utility, something](){} ); return 0; } 


控制台输出
实例()
SingletonShared()
SharedSingleThreadedUtility()
SomethingWithVeryImportantDestructor()

已创建一个单例。

实用程序已创建。

创建了S-非常重要的析构函数(我添加这个词是为了吓tim,因为Internet上有诸如“好吧,单例析构函数将不会被调用,所以这是什么,它必须一直存在程序”)。

但是对于这些对象中的任何一个都没有调用析构函数!

因为什么 由于用目标代替手段。

单例弱


单例弱
 #pragma once #include <memory> #include <iostream> template<typename T> class SingletonWeak : public T { public: ~SingletonWeak() { std::cout << "~SingletonWeak()" << std::endl; } SingletonWeak(const SingletonWeak &) = delete; SingletonWeak(SingletonWeak &&) = delete; SingletonWeak& operator=(const SingletonWeak &) = delete; SingletonWeak& operator=(SingletonWeak &&) = delete; static std::weak_ptr<SingletonWeak> instance() { std::cout << "instance()" << std::endl; // "new" and no std::make_shared because of private c-tor static auto inst = std::shared_ptr<SingletonWeak>(new SingletonWeak); return inst; } private: SingletonWeak() { std::cout << "SingletonWeak()" << std::endl; } }; 


如果有的话,对开源中的单例进行这样的修改当然不是经常的。 我遇到了一些奇怪的变体,它们的内部似乎都使用了std :: weak_ptr,似乎可以使用它,它似乎提供的实用程序不过是延长了单例的寿命而已:


我建议的选项在单例和实用程序层中正确应用时:

  • 防止上述示例中描述的用户层中的操作,包括防止死锁;
  • 比Classic_Example3_correct中的thread_local应用程序更准确地确定应用程序折叠的时刻,即 让您更靠近边缘;
  • 我不会遭受用均值代替目标的理论问题的困扰(我不知道除了僵局以外,其他任何有形的东西都可能会从该理论问题中出现)。

但是,这样做有一个缺点:延长单身人士的寿命仍然可以使它更靠近边缘。

SingletonWeak示例1


类似于Shared_Example3_correct.cpp。

Weak_Example1_correct.cpp
 #include "SingletonWeak.h" #include "Payload.h" #include <memory> class WeakSingleThreadedUtility { public: WeakSingleThreadedUtility() // To ensure that singleton will be constucted before utility : m_weak(SingletonWeak<Payload>::instance()) { } ~WeakSingleThreadedUtility() { // Sometimes this check may result as "false" even in case of incorrect usage, // and there's no way to guarantee a demonstration of undefined behaviour in colour if ( auto strong = m_weak.lock() ) for ( int i = 0; i < 100; ++i ) strong->add(i); } private: // A weak copy of smart pointer, not a reference std::weak_ptr<SingletonWeak<Payload>> m_weak; }; // 1. Create an empty unique_ptr // 2. Create singleton (because of WeakSingleThreadedUtility c-tor) // 3. Create utility std::unique_ptr<WeakSingleThreadedUtility> emptyUnique; auto utilityUnique = std::make_unique<WeakSingleThreadedUtility>(); int main() { // This guarantee destruction in order: // - utilityUnique; // - singleton; // - emptyUnique. // This order is correct ... // ... but user swaps unique_ptrs emptyUnique.swap(utilityUnique); // Guaranteed destruction order is the same: // - utilityUnique; // - singleton; // - emptyUnique, // but now utilityUnique is empty, and emptyUnique is filled, // so destruction order is incorrect... // ... but utility have made a weak copy of shared_ptr when it was available, // so it's correct again. return 0; } 


控制台输出
实例()
单例弱()
〜SingletonWeak()

为什么我们需要SingletonWeak,因为没有人打扰实用程序将SingletonShared用作SingletonWeak? 是的,没有人打扰。 甚至没有人打扰该实用程序使用SingletonWeak作为SingletonShared。 但是,将它们用于预期目的要比将它们用于其他目的要容易一些。

SingletonWeak示例2


与Shared_Example4_incorrect相似,但是在这种情况下不会发生死锁。

Weak_Example2_correct.cpp
 #include "SingletonWeak.h" #include "CallbackPayload.h" #include "SomethingWithVeryImportantDestructor.h" class WeakSingleThreadedUtility { public: WeakSingleThreadedUtility() // To ensure that singleton will be constucted before utility : m_weak(SingletonWeak<CallbackPayload>::instance()) { std::cout << "WeakSingleThreadedUtility()" << std::endl; } ~WeakSingleThreadedUtility() { std::cout << "~WeakSingleThreadedUtility()" << std::endl; } void setCallback(std::function<void()> &&fn) { if ( auto strong = m_weak.lock() ) strong->setCallback(std::move(fn)); } private: // A weak copy of smart pointer, not a reference std::weak_ptr<SingletonWeak<CallbackPayload>> m_weak; }; int main() { auto utility = std::make_shared<WeakSingleThreadedUtility>(); auto something = std::make_shared<SomethingWithVeryImportantDestructor>(); // lambda with "utility" and "something" captured utility->setCallback( [utility, something](){} ); return 0; } 


控制台输出
实例()
单例弱()
WeakSingleThreadedUtility()
SomethingWithVeryImportantDestructor()
〜SingletonWeak()
〜SomethingWithVeryImportantDestructor()
〜WeakSingleThreadedUtility()

而不是结论


而且,单例的这种修改会消除未定义的行为吗? 我保证不会有幸福的结局。 下面的示例显示,用户层中的熟练破坏活动甚至可以单枪甚至摧毁正确的思想库(但我们必须承认, 很难偶然完成)。

Shared_Example5_incorrect.cpp
 #include "SingletonShared.h" #include "Payload.h" #include <memory> #include <cstdlib> class SharedSingleThreadedUtility { public: SharedSingleThreadedUtility() // To ensure that singleton will be constucted before utility : m_singleton(SingletonShared<Payload>::instance()) { } ~SharedSingleThreadedUtility() { // Sometimes this check may result as "false" even for destroyed singleton // preventing from visual effects of undefined behaviour ... //if ( m_singleton ) // for ( int i = 0; i < 100; ++i ) // m_singleton->add(i); // ... so this code will allow to demonstrate UB in colour for ( int i = 0; i < 100; ++i ) m_singleton->add(i); } private: // A copy of smart pointer, not a reference std::shared_ptr<SingletonShared<Payload>> m_singleton; }; void cracker() { SharedSingleThreadedUtility(); } // 1. Register cracker() using std::atexit // 2. Create singleton // 3. Create utility auto reg = [](){ std::atexit(&cracker); return 0; }(); auto utility = SharedSingleThreadedUtility(); // This guarantee destruction in order: // - utility; // - singleton. // This order is correct. // Additionally, there's a copy of shared_ptr in the class instance... // ... but there was std::atexit registered before singleton, // so cracker() will be invoked after destruction of utility and singleton. // There's second try to create a singleton - and it's incorrect. int main() { return 0; } 


控制台输出
实例()
SingletonShared()
〜SingletonShared()
实例()

Weak_Example3_incorrect.cpp
 #include "SingletonWeak.h" #include "Payload.h" #include <memory> #include <cstdlib> class WeakSingleThreadedUtility { public: WeakSingleThreadedUtility() // To ensure that singleton will be constucted before utility : m_weak(SingletonWeak<Payload>::instance()) { } ~WeakSingleThreadedUtility() { // Sometimes this check may result as "false" even in case of incorrect usage, // and there's no way to guarantee a demonstration of undefined behaviour in colour if ( auto strong = m_weak.lock() ) for ( int i = 0; i < 100; ++i ) strong->add(i); } private: // A weak copy of smart pointer, not a reference std::weak_ptr<SingletonWeak<Payload>> m_weak; }; void cracker() { WeakSingleThreadedUtility(); } // 1. Register cracker() using std::atexit // 2. Create singleton // 3. Create utility auto reg = [](){ std::atexit(&cracker); return 0; }(); auto utility = WeakSingleThreadedUtility(); // This guarantee destruction in order: // - utility; // - singleton. // This order is correct. // Additionally, there's a copy of shared_ptr in the class instance... // ... but there was std::atexit registered before singleton, // so cracker() will be invoked after destruction of utility and singleton. // There's second try to create a singleton - and it's incorrect. int main() { return 0; } 


控制台输出
实例()
单例弱()
〜SingletonWeak()
实例()

Source: https://habr.com/ru/post/zh-CN455848/


All Articles