在我的项目中,我们有一些单例,这在单元测试中往往是有问题的。所以我想找出解决问题的办法。到目前为止,我带着的是:
smart_singleton.h
class smart_singleton
{
public:
static std::shared_ptr<smart_singleton> get_instance();
private:
smart_singleton();
smart_singleton(const smart_singleton&) = delete;
smart_singleton operator=(const smart_singleton&) = delete;
smart_singleton(smart_singleton&&) = default;
smart_singleton& operator=(smart_singleton&&) = default;
static std::weak_ptr<smart_singleton> weak_instance;
};smart_singleton.cpp
std::weak_ptr<smart_singleton> smart_singleton::weak_instance;
std::shared_ptr<smart_singleton> smart_singleton::get_instance()
{
if (auto existing_instance = weak_instance.lock()) {
return existing_instance;
} else {
std::shared_ptr<smart_singleton> tmp_shared(new smart_singleton());
weak_instance = tmp_shared;
return tmp_shared;
}
}
smart_singleton::smart_singleton()
{
}不同之处在于,您需要保存代码中任何地方的"get_instance()“中的一个get_instance,以便对象不被销毁。在您的生成代码中,它将位于主函数的某个位置(或者某个对象在main的整个范围内都是活动的)。在UT中,这将持续一次测试。
重要信息:
请不要将注意力集中在单例模式本身被劝阻。我的项目有单身汉,改变这意味着要付出很大的努力。我的尝试只是使单元测试更容易。因此,我感兴趣的是,在C++中拥有一个经典的单例实现与我的实现之间的区别,以及后一个可能导致的问题。
发布于 2016-11-20 06:50:15
我喜欢这个主意。
首先,get_instance()方法似乎不是线程安全的:
std::shared_ptr smart_singleton::get_instance() { if (autoexisting_instance= weak_instance.lock()) {返回existing_instance;}weak_instance.lock{ std::shared_ptr tmp_shared(新的smart_singleton());weak_instance = tmp_shared;返回tmp_shared;}
它并不是线程安全的,但是在这种情况下,文档清楚地说明这一点非常重要。但是,如果您要走这条路线,就应该像这样将get_instance与创建分离开来:
std::shared_ptr<smart_singleton> smart_singleton::create_instance()
{
if (weak_instance.lock()) {
throw already_instantiated_error{}; // something better
}
std::shared_ptr<smart_singleton> tmp_shared(new smart_singleton());
weak_instance = tmp_shared;
return tmp_shared;
}
std::shared_ptr<smart_singleton> smart_singleton::get_instance()
{
return weak_instance.lock();
}然后,您可以在文档中注意到,如果没有实例,get_instance()返回一个nullptr;在“全局”范围(如main)中,它们应该调用auto keep_alive = smart_singleton::create_instance();。
smart_singleton.h需要头部保护,它应该包括<memory>:
#ifndef SMART_SINGLETON_H_
#define SMART_SINGLETON_H_
#include <memory>
...
#endif对我来说,这个设计的主要问题是它很容易使用错误。任何类缓存单例的静态副本都会导致单元测试失败。单元测试必须盲目地进入,并假定任何地方都没有违规的构造。
最好有一个smart_singleton::reset(),它重置单元测试可以调用的所有存储在单例中的数据。
如果我是这样做的话,我会将它变成一个CRTP类,这样创建smart_singletons就变得非常容易了:
template<class T>
class smart_singleton
{
public:
static std::shared_ptr<T> get_instance()
{
if (auto existing_instance = weak_instance.lock()) {
return existing_instance;
} else {
std::shared_ptr<T> tmp_shared(new T());
weak_instance = tmp_shared;
return tmp_shared;
}
}
protected:
smart_singleton() {}
private:
smart_singleton(const smart_singleton<T>&) = delete;
smart_singleton operator=(const smart_singleton<T>&) = delete;
smart_singleton(smart_singleton<T>&&) = default;
smart_singleton& operator=(smart_singleton<T>&&) = default;
static std::weak_ptr<T> weak_instance;
};
template<class T>
std::weak_ptr<T> smart_singleton<T>::weak_instance;现在,要使一个类成为单例,这就是我要做的所有事情:
class my_singleton : public smart_singleton<my_singleton>
{
friend class smart_singleton<my_singleton>; // so that the smart-singleton can access the private constructor of this class
public:
...
private:
my_singleton();
};发布于 2017-03-28 05:15:30
正如我在另一个答案中提到的,这个测试单例的解决方案很容易使用错误。单个实例的任何缓存实例都会使您的单元测试出错。但是,还有一种使您的单例单元测试变得友好的方法:让单例实现一个接口。
一个真实的例子是Eclipse的Ecore。Ecore是一个为您生成代码的Java建模框架。它生成的每个包都配备了一个MyPackageFactory --实际上是与MyPackageFactory.eINSTANCE.createFoo()一起使用的单例程序包。然而,MyPackageFactory实际上是一个接口;单例是用MyPackageFactoryImpl实现的。这意味着您可以将单例注入到类中,允许您模拟单元测试。
作为一个粗略的例子:
#pragma once
class MySingleton {
public:
static MySingleton &get_instance();
// simply define an interface
virtual void do_something() = 0;
virtual void set_something(int i) = 0;
};#include "my-singleton.h"
#include <iostream>
struct MySingletonImpl final : MySingleton {
int value;
void do_something() override {
std::cout << value << '\n';
}
void set_something(int i) override {
value = i;
}
};
MySingleton &MySingleton::get_instance() {
static MySingletonImpl instance;
return instance;
}然后,它可以以一种友好的单元测试方式使用,如下所示:
#pragma once
#include "my-singleton.h"
struct UsesSingleton {
MySingleton *singleton;
int i;
explicit UsesSingleton(MySingleton &singleton, int i)
: singleton{ &singleton }
, i{ i }
{}
void do_something() {
singleton->do_something();
singleton->set_something(i);
singleton->do_something();
}
};#include "uses-singleton.h"
int main() {
UsesSingleton myStruct{ MySingleton::get_instance(), 10 };
myStruct.do_something();
}使用-singleton-test.cpp-(仅以googlemock为例。这也是未经测试的)
#include "gtest/gtest.h"
#include "gmock/gmock.h"
class MySingletonMock : public MySingleton {
public:
MOCK_METHOD0(do_something, void());
MOCK_METHOD1(set_something, void(int))
};
TEST(UsesSingletonTests, SampleTest) {
MySingletonMock singleton;
{
::testing::InSequence dummy;
EXPECT_CALL(singleton, do_something());
EXPECT_CALL(singleton, set_something(10));
EXPECT_CALL(singleton, do_something());
}
UsesSingleton myStruct{ singleton, 10 };
myStruct.do_something();
}请注意,这很可能导致在运行时进行虚拟表查找,除非您在头文件中声明MySingletonImpl并在头文件中定义get_instance():
#pragma once
class MySingleton {
public:
static MySingleton &get_instance();
// simply define an interface
virtual void do_something() = 0;
virtual void set_something(int i) = 0;
};
namespace detail {
struct MySingletonImpl final : MySingleton {
int value;
void do_something() override;
void set_something(int i) override;
private:
// We have to hide it now because it was previously
// hidden by virtue of being in a cpp file
MySingletonImpl() = default;
friend MySingleton &MySingleton::get_instance();
};
}
MySingleton &MySingleton::get_instance() {
static detail::MySingletonImpl instance;
return instance;
}#include "my-singleton.h"
#include <iostream>
void detail::MySingletonImpl::do_something() {
std::cout << value << '\n';
}
void detail::MySingletonImpl::set_something(int i) {
value = i;
}然后编译器可能会将它优化到正确的函数调用,特别是如果get_instance是detail::MySingletonImpl &get_instance(),但我不认为这会造成任何问题。
无论哪种方式,性能损失可能都很小,所以这可能不值得你花时间。
https://codereview.stackexchange.com/questions/147506
复制相似问题