C++ 中干净、类似函数式脚本的错误处理

clean, functional script-like error handling in c++

本文关键字:脚本 错误 处理 函数 C++      更新时间:2023-10-16

这是一个有趣的挑战。

我想创建一些样板,允许使用类似于 perl 的x or die("reason")方法的语法干净地处理前提条件失败。

我想出了这个删除器:

struct do_exit {
[[noreturn]]
void operator()(void*) const noexcept {
std::exit(code_);
}
int code_;
};

我们可以用来管理临时std::unique_ptr的"删除",这可能指向std::cerr

struct exit_stream
{
exit_stream(int code, std::ostream& os)
: stream_(std::addressof(os), do_exit { code })
{}
std::ostream& as_stream() const { return *stream_; }
operator bool() const { return bool(*stream_); }
template<class T>
friend auto operator<<(const exit_stream& es, T const& t) -> exit_stream const&
{
es.as_stream() << t;
return es;
}
std::unique_ptr<std::ostream, do_exit> stream_;
};

使用以下功能创建:

exit_stream die(int code = 100)
{
return exit_stream(code, std::cerr);
}

现在我们可以编写以下程序:

int main(int argc, char* argv[]) {
// preconditions
argc >= 2 or die(4) << "usage: myprog <inputfile>n";
auto input = std::ifstream(argv[1]);
input or die(100) << "failed to open " << argv[1] << "n";
}

这是因为当exit_stream作为临时返回时,必须移动它(unique_ptr删除复制运算符)。unique_ptr保证移自指针将为空。它还保证仅在unique_ptr不为 null 时才调用删除程序。因此,只有在调用站点执行所有流式处理操作后,才会调用 exit。

但是,明显的失败是我不会写:

auto input = std::ifstream(argv[1]]) or die() << "failed to open " << argv[1] << "n";

因为如果我这样做,input会是一个布尔人。

这是我的问题。你能想到一种方法来产生所需的效果,或者尽可能接近它吗?

我真的很想避免这种情况,出于通过情况下的效率原因,以及为了清晰和优雅:

auto input = (std::ifstream(argv[1]]) or die() << "failed to open " << argv[1] << "n").get();

完整代码:

#include <fstream>
#include <iostream>
#include <cstdlib>
struct do_exit {
[[noreturn]]
void operator()(void*) const noexcept {
std::exit(code_);
}
int code_;
};
struct exit_stream
{
exit_stream(int code, std::ostream& os)
: stream_(std::addressof(os), do_exit { code })
{}
std::ostream& as_stream() const { return *stream_; }
operator bool() const { return bool(*stream_); }
template<class T>
friend auto operator<<(const exit_stream& es, T const& t) -> exit_stream const&
{
es.as_stream() << t;
return es;
}
std::unique_ptr<std::ostream, do_exit> stream_;
};
exit_stream die(int code = 100)
{
return exit_stream(code, std::cerr);
}
int main(int argc, char* argv[]) {
// preconditions
argc >= 2 or die(4) << "usage: myprog <inputfile>n";
auto input = std::ifstream(argv[1]);
input or die(100) << "failed to open " << argv[1];
}

好的,这是我的第一次尝试。由于缺乏短路,我有点担心冗余结构,但语法是我想要的。

欢迎评论/改进!

#include <fstream>
#include <iostream>
#include <utility>
#include <iomanip>
namespace detail {
struct tuple_emitter {
tuple_emitter(std::ostream &os) : os(os) {}
template<class...Refs>
void operator()(std::tuple<Refs...> const &t) const {
emit_impl(t, std::make_index_sequence<sizeof...(Refs)>());
}
private:
template<class T>
void emit_impl(T const &t) const {
os << t;
}
// make sure we handle lazy manipulators
void emit_impl(std::ios_base (* f)(std::ios_base &)) const
{
f(os);
}
template<class Tie, std::size_t...Is>
void emit_impl(Tie const &refs, std::index_sequence<Is...>) const {
using expand = int[];
void(expand{0,
((emit_impl(std::get<Is>(refs))), 0)...});
os << std::endl;
};

std::ostream &os;
};
// a class that emits a number of references to std::cerr and then
// exits    
template<class...Refs>
struct dier {
dier(int code, std::tuple<Refs...> t) : code(code), emitter(std::cout), refs_(t) {}
template<class...Others, class Ref>
dier(dier<Others...> const &original, Ref const &r)
: code(original.code), emitter(original.emitter), refs_(std::tuple_cat(original.refs_, std::tie(r))) {};
void operator()() const {
emitter(refs_);
std::exit(code);
}
int code;
tuple_emitter emitter;
std::tuple<Refs&...> refs_;
};
template<class...Refs, class Ref>
auto operator<<(dier<Refs...> original, Ref const &ref) {
return dier<Refs..., const Ref &>(original, ref);
};

template<class T, class...Refs>
T operator||(T &&t, dier<Refs...> death) {
if (!t) {
death();
}
return std::move(t);
};
template<class T, class...Refs>
T const &operator||(T &t, dier<Refs...> death) {
if (!t) {
death();
}
return t;
};
}
auto die(int code = 100) {
return detail::dier<>(code, std::make_tuple());
}
int main(int argc, char *argv[]) {
// preconditions
argc >= 2 or die(4) << "usage: myprog <inputfile>n";
auto input = std::ifstream(argv[1]) or die(5) << "failed to open " << std::setfill('-') << std::setw(10) << argv[1] << " and here is a hex number " << std::hex << 40;
}

预期产出:

没有参数:

usage: myprog <inputfile>

用一个参数"foo.txt":

failed to open ---foo.txt and here is a hex number 28

雅克建议后更新

把它归结为这个 - 这更干净:

#include <fstream>
#include <iostream>
#include <utility>
#include <iomanip>
namespace notstd {
namespace detail {
template<class F, class T, std::size_t...Is>
void apply_impl(F&& f, T&& t, std::index_sequence<Is...>)
{
using expand = int[];
void(expand{0,
((f(std::get<Is>(t))), 0)...});
};
}
template<class F, class...Ts>
void apply(F&& f, std::tuple<Ts...> const& t)
{
detail::apply_impl(std::forward<F>(f), t, std::make_index_sequence<sizeof...(Ts)>());
};
struct apply_to_stream
{
apply_to_stream(std::ostream& os)
: os(os)
{}
template<class T>
std::ostream& operator()(T&& t) const
{
return os << t;
};
std::ostream& operator()(std::ios_base& (*f)(std::ios_base&)) const
{
f(os);
return os;
};
std::ostream& os;
};

}
namespace detail {
template<class Tuple>
struct dier {
constexpr dier(int code, Tuple t) : code(code), refs_(std::move(t)) {}
void operator()() const {
notstd::apply(notstd::apply_to_stream(std::cout), refs_);
std::cout << 'n';
std::exit(code);
}
int code;
Tuple refs_;
};
template<class Tie>
constexpr auto make_dier(int code, Tie&& t) {
return detail::dier<Tie>(code, std::forward<Tie>(t));
}
template<class Tuple, class Ref>
constexpr auto operator<<(dier<Tuple> original, Ref&& ref) {
return make_dier(original.code, std::tuple_cat(original.refs_, std::tie(ref)));
}

template<class T, class...Refs>
T operator||(T &&t, dier<Refs...> death) {
if (!t) {
death();
}
return std::forward<T>(t);
}
}
template<class Tie = std::tuple<>>
constexpr auto die(int code = 100, Tie&& t = {}) {
return detail::make_dier(code, std::forward<Tie>(t));
}
int main(int argc, char *argv[]) {
// preconditions
argc >= 2 or die(4) << "usage: myprog <inputfile>n";
auto input = std::ifstream(argv[1]) or die(5) << "failed to open " << std::setfill('-') << std::setw(10) << argv[1] << " and here is a hex number " << std::hex << 40;
}

这是我试图提高@RichardHodges自我答案的尝试。

首先,一些库式代码:

namespace notstd {
// takes a function object
// returns a function object that calls the first function object once for each argument
template<class F>
auto foreach( F&& f ) {
return [f=std::forward<F>(f)](auto&&...args)mutable{
using discard=int[];
(void)discard{0,(void(
f(decltype(args)(args))
),0)...};
};
}
// weak version of std::apply:
template<std::size_t...Is, class F, class Tuple>
decltype(auto) apply( std::index_sequence<Is...>, F&& f, Tuple&& tup) {
return std::forward<F>(f)( std::get<Is>(std::forward<Tuple>(tup))... );
}
template<class F, class Tuple>
decltype(auto) apply(F&& f, Tuple&& tup) {
auto indexes = std::make_index_sequence< std::tuple_size< std::remove_reference_t<Tuple> >{} >{};
return apply(indexes, std::forward<F>(f), std::forward<Tuple>(tup) );
}
}

现在我们使用它。 由于上述原因,此代码更简短:

namespace death {
namespace details {
template<class Tuple>
struct dier;
}
template<class Tuple=std::tuple<>>
details::dier<Tuple> die( int code, std::ostream& os = std::cerr, Tuple&& tuple={} ) {
return {code, os, std::forward<Tuple>(tuple)};
}
namespace details {
// a class that emits a number of references to std::cerr and then
// exits  
template<class Tuple>
struct dier {
void operator()() const {
notstd::apply( notstd::foreach( [&](auto const&e){ os << e; } ), refs_ );
os << std::endl;
std::exit(code);
}
int code;
std::ostream& os;
Tuple refs_;
template<class Ref>
friend auto operator<<(dier&& original, Ref const &ref) {
return die( original.code, original.os,  std::tuple_cat( std::move(original).refs_, std::tie(ref) ) );
}
template<class T>
friend T operator||(T &&t, dier&& death) {
if (!t) {
death();
}
return std::forward<T>(t);
}
};
}
}
auto die(int code = 100, std::ostream& os = std::cerr) {
return death::die(code, os);
}

希望有帮助。

活生生的例子。

请注意,除实用程序代码外,所有...都将被消除。