我有一个从外部文件加载的类,因此理想情况下,如果加载失败,我希望它的构造函数从给定路径加载,如果找不到/不可读文件,我将抛出错误(从构造函数抛出错误并不是一个可怕的主意,请参阅ISO's FAQ)。
这是有问题的,我想以某种受控方式自己处理错误,并且我想立即执行此操作,因此我需要在该对象的构造函数周围放置一个try-catch语句...并且如果这样做,则不会在try语句之外声明该对象,即:
//in my_class.hpp
class my_class
{
...
public:
my_class(string path);//Throws file not found, or other error error
...
};
//anywhere my_class is needed
try
{
my_class my_object(string);
}
catch(/*Whatever error I am interesetd in*/)
{
//error handling
}
//Problem... now my_object doesn't exist anymore
我尝试了多种方法来解决它,但是我真的不喜欢其中任何一种方法:
首先,我可以使用指向my_class的指针而不是类本身:
my_class* my_pointer;
try
{
my_class my_pointer = new my_class(string);
}
catch(/*Whatever error I am interesetd in*/)
{
//error handling
}
问题在于此对象的实例并不总是以创建该对象的对象结尾,因此正确删除所有指针很容易出错,此外,我个人认为拥有一些指针是很丑陋的对象是指向对象的指针,而大多数是“常规对象”。
第二,我可以使用与一个元素相同的矢量:
std::vector<my_class> single_vector;
try
{
single_vector.push_back(my_class(string));
single_vector.shrink_to_fit();
}
catch(/*Whatever error I am interesetd in*/)
{
//error handling
}
我不喜欢有很多单元素矢量的想法。
第三,我可以创建一个空的人造构造函数并使用另一个加载函数,即
//in my_class.hpp
class my_class
{
...
public:
my_class() {}// Faux constructor which does nothing
void load(string path);//All the code in the constructor has been moved here
...
};
//anywhere my_class is needed
my_class my_object
try
{
my_object.load(path);
}
catch(/*Whatever error I am interesetd in*/)
{
//error handling
}
这行得通,但是在很大程度上违背了使用构造函数的目的,因此我也不是很喜欢。
所以我的问题是,哪种构造对象的方法最好(或最少坏),这可能会在构造函数中引发错误?还有更好的方法吗?
因为该对象可能需要在程序首次启动时创建,然后在很长时间后停止。在最极端的情况下(在这种情况下我实际上也确实需要)实际上是:
int main()
{
try
{
//... things which might fail
//A few hundred lines of code
}
catch(/*whaveter*/)
{
}
}
我认为这会使我的代码难以阅读,因为catch语句与实际出错的地方相距很远。
答案 0 :(得分:2)
一种可能性是将构造和错误处理包装在一个函数中,返回构造的对象。例子:
#include <string>
class my_class {
public:
my_class(std::string path);
};
my_class make_my_object(std::string path)
{
try {
return {std::move(path)};
}
catch(...) {
// Handle however you want
}
}
int main()
{
auto my_object = make_my_object("this path doesn't exist");
}
但是请注意,该示例是不完整的,因为不清楚在构造失败时您打算做什么。 catch
块必须返回,抛出或终止。
my_class(std::string path)
中将实例初始化为该状态。因此,在那种情况下,不需要try
/ catch
块。try
/ catch
块,除非您想做一些额外的工作,例如日志记录。try
/ catch
块。这里真正的解决方案可能是根本不使用try
/ catch
块,除非实际上存在错误处理,否则您不应该将其实现为{{1}的一部分}这在问题中没有体现出来(也许是后备路径?)。
答案 1 :(得分:1)
如果执行此操作,则不会在try语句之外声明该对象
我尝试了多种解决方法
那不一定是问题。不一定需要解决它。只需在try语句中使用该对象即可。
如果您真的无法在整个生命周期中都使用try块,那么这是std::optional
的用例:
std::optional<my_class> maybe_my_object;
try {
maybe_my_object.emplace(string);
} catch(...) {}
问题在于此对象的实例并不总是以创建该对象的对象结尾,因此正确删除所有指针很容易出错,
由new
返回的指针可以正确删除。在错误情况下,只需将指针设置为null,就不会有问题。也就是说,如果要使用这种方法,请改为使用智能指针进行动态分配。
single_vector.push_back(my_class(string)); single_vector.shrink_to_fit();
当您知道向量中将要包含的对象数量时,不要推和缩小。如果要使用此方法,请改用reserve
。
答案 2 :(得分:1)
由于资源不可用,对象创建失败。失败的不是创造;而是失败。这是一个无法满足的先决条件。
因此,将这两个问题分开:首先获取所有资源,然后(如果成功的话)使用这些资源创建对象并使用它。这样的设计中的对象创建不会失败,构造函数不会抛出异常。这是简单的样板代码(复制数据等)。另一方面,如果资源获取失败,则对象创建和对象使用都将被跳过:现有但无法使用的对象的问题就消失了。
响应您对包含整个程序的try / catch的编辑:作为错误指示符的异常更适合在程序中的不同时间在许多地方完成的事情,因为它们保证了错误处理(默认情况下通过中止)将其与正常控制流分开。经典的返回值检查无法做到这一点,这使我们只能在不可读或不可靠的程序之间进行选择。
但是,如果您拥有的寿命很长的对象很少创建(在您的示例中:仅在启动时),则不需要例外。如您所说,构造函数异常保证只能使用正确初始化的对象。但是,如果仅在启动时创建这样的对象,则危险很低。您以一种或另一种方式检查是否成功,如果初始资源获取失败,则退出无法执行其目的的程序。这样就可以在发生错误的地方进行处理。即使在不太极端的情况下(例如,当在main以外的大型函数的开头创建对象时),这也可能是更简单的解决方案。
在代码中,我的建议如下:
struct T2;
struct myEx { myEx(const char *); };
void exit(int);
T1 *acquireResource1(); // e.g. read file
T2 *acquireResource2(); // e.g. connect to db
void log(const char *what);
class ObjT
{
public:
struct RsrcT
{
T1 *mT1;
T2 *mT2;
operator bool() { return mT1 && mT2; }
};
ObjT(const RsrcT& res) noexcept
{
// initialize from file data etc.
}
// more member functions using data from file and db
};
int main()
{
ObjT::RsrcT rsrc = { acquireResource1(), acquireResource2() };
if(!rsrc)
{
log("bummer");
exit(1);
}
///////////////////////////////////////////////////
// all resources are available. "Real" code starts here.
///////////////////////////////////////////////////
ObjT obj(rsrc);
// 1000 lines of code using obj
}