From 649a5ee02a311988fdf8404f163eb1575ad814d5 Mon Sep 17 00:00:00 2001 From: mutouyun Date: Sun, 25 Dec 2022 13:25:29 +0800 Subject: [PATCH] upd: [imp] optimize result interface and fix some bugs --- include/libimp/error.h | 1 + include/libimp/fmt_cpo.h | 4 + include/libimp/result.h | 138 ++++++++++++--------------- src/libimp/error.cpp | 11 ++- src/libimp/fmt.cpp | 18 +--- src/libimp/result.cpp | 36 ------- src/libipc/platform/posix/shm_impl.h | 46 +++++---- src/libipc/platform/win/mmap_impl.h | 12 +-- src/libipc/platform/win/shm_impl.h | 6 +- test/imp/test_imp_result.cpp | 76 ++++++++------- 10 files changed, 156 insertions(+), 192 deletions(-) delete mode 100644 src/libimp/result.cpp diff --git a/include/libimp/error.h b/include/libimp/error.h index ab0c60a..af35bde 100644 --- a/include/libimp/error.h +++ b/include/libimp/error.h @@ -54,6 +54,7 @@ class LIBIMP_EXPORT error_code { public: /// \brief constructors error_code() noexcept; + error_code(error_code_t const &r) noexcept; error_code(error_code_t const &r, error_category const &ec) noexcept; /// \brief observers diff --git a/include/libimp/fmt_cpo.h b/include/libimp/fmt_cpo.h index ef17a1f..c3951b8 100644 --- a/include/libimp/fmt_cpo.h +++ b/include/libimp/fmt_cpo.h @@ -8,6 +8,7 @@ #include #include +#include #include "libimp/def.h" #include "libimp/generic.h" @@ -18,9 +19,12 @@ LIBIMP_NAMESPACE_BEG_ /** + * \class class LIBIMP_EXPORT fmt_context * \brief The context of fmt. */ class LIBIMP_EXPORT fmt_context { + std::array sbuf_; ///< stack buffer + std::string &joined_; std::size_t offset_; diff --git a/include/libimp/result.h b/include/libimp/result.h index f7b37ca..7ac6fa7 100644 --- a/include/libimp/result.h +++ b/include/libimp/result.h @@ -16,39 +16,9 @@ #include "libimp/export.h" #include "libimp/fmt.h" #include "libimp/generic.h" +#include "libimp/error.h" LIBIMP_NAMESPACE_BEG_ - -using result_code_t = std::uint64_t; -using result_type = std::tuple; - -/** - * \class class LIBIMP_EXPORT result_code - * \brief Uses std::uint64_t as the default underlying type of error code. - */ -class LIBIMP_EXPORT result_code { - result_type status_; - -public: - result_code() noexcept; - result_code(result_code_t value) noexcept; - result_code(result_type const &value) noexcept; - result_code(bool ok, result_code_t value) noexcept; - - result_code_t value() const noexcept; - bool ok() const noexcept; - - result_code_t operator*() const noexcept { - return value(); - } - explicit operator bool() const noexcept { - return ok(); - } - - friend bool operator==(result_code const &lhs, result_code const &rhs) noexcept; - friend bool operator!=(result_code const &lhs, result_code const &rhs) noexcept; -}; - namespace detail_result { template @@ -60,31 +30,56 @@ template > class result; +/// \typedef Uses std::uint64_t as the default underlying type of result code. +using result_code = result; + namespace detail_result { template -struct default_traits::value>> { +struct generic_traits { + /// \typedef Combine data and valid identifiers with a tuple. + using storage_t = std::tuple; + /// \brief Custom initialization. - constexpr static void init_code(result_code &code) noexcept { - code = {}; + constexpr static void init_code(storage_t &code) noexcept { + code = {0, -1/*make a default error code*/}; } - constexpr static void init_code(result_code &code, bool ok, T value) noexcept { - code = {ok, static_cast(value)}; + constexpr static void init_code(storage_t &code, T value, error_code const &ec) noexcept { + code = {value, ec}; } - constexpr static void init_code(result_code &code, T value) noexcept { - init_code(code, true, value); + constexpr static void init_code(storage_t &code, T value) noexcept { + code = {value, {}}; } + constexpr static void init_code(storage_t &code, error_code const &ec) noexcept { + code = {{}, ec}; + } + + /// \brief Custom type data acquisition. + constexpr static T get_value(storage_t const &code) noexcept { + return std::get<0>(code); + } + constexpr static bool get_ok(storage_t const &code) noexcept { + return !std::get<1>(code); + } + constexpr static error_code get_error(storage_t const &code) noexcept { + return std::get<1>(code); + } +}; + +template +struct default_traits::value>> : generic_traits { + /// \brief Custom initialization. + constexpr static void init_code(storage_t &code, T value, bool ok) noexcept { + code = {value, static_cast(ok ? 0 : + ((value == default_value()) ? -1 : value))}; + } + using generic_traits::init_code; /// \brief Custom default value. constexpr static T default_value() noexcept { return 0; } - /// \brief Custom type conversions. - constexpr static T cast_from_code(result_code code) noexcept { - return static_cast(code.value()); - } - /// \brief Custom formatted output. static std::string format(result const &r) noexcept { return fmt(*r); @@ -92,34 +87,27 @@ struct default_traits::value>> { }; template -struct default_traits::value>> { +struct default_traits::value>> : generic_traits { /// \brief Custom initialization. - constexpr static void init_code(result_code &code, T value = default_value()) noexcept { - code = {default_value() != value, reinterpret_cast(value)}; + constexpr static void init_code(storage_t &code, std::nullptr_t, error_code const &ec) noexcept { + code = {nullptr, ec}; } - constexpr static void init_code(result_code &code, std::nullptr_t) noexcept { - code = {false, {}}; - } - constexpr static void init_code(result_code &code, std::nullptr_t, result_code_t r) noexcept { - code = {false, r}; + constexpr static void init_code(storage_t &code, std::nullptr_t) noexcept { + code = {nullptr, -1}; } + using generic_traits::init_code; /// \brief Custom default value. constexpr static T default_value() noexcept { return nullptr; } - /// \brief Custom type conversions. - constexpr static T cast_from_code(result_code code) noexcept { - return code.ok() ? reinterpret_cast(code.value()) : default_value(); - } - /// \brief Custom formatted output. static std::string format(result const &r) noexcept { if LIBIMP_LIKELY(r) { return fmt(static_cast(*r)); } - return fmt(static_cast(*r), ", code = ", r.code_value()); + return fmt(static_cast(*r), ", error = ", r.error()); } }; @@ -127,51 +115,43 @@ struct default_traits::value>> { /** * \class class result - * \brief The generic wrapper for the result_code. + * \brief The generic wrapper for the result type. */ template class result : public TypeTraits { - - /// \brief Internal data is stored using result_code. - result_code code_; - public: using type_traits_t = TypeTraits; + using storage_t = typename type_traits_t::storage_t; +private: + storage_t code_; ///< internal data + +public: template , - typename = decltype(type_traits_t::init_code(std::declval() + typename = decltype(type_traits_t::init_code(std::declval() , std::declval()...))> result(A &&... args) noexcept { type_traits_t::init_code(code_, std::forward(args)...); } - bool ok() const noexcept { return code_.ok(); } - T value() const noexcept { return type_traits_t::cast_from_code(code_); } + T value() const noexcept { return type_traits_t::get_value(code_); } + bool ok () const noexcept { return type_traits_t::get_ok (code_); } + error_code error() const noexcept { return type_traits_t::get_error(code_); } - result_code_t code_value() const noexcept { return code_.value(); } - - T operator*() const noexcept { - return value(); - } - explicit operator bool() const noexcept { - return ok(); - } + T operator * () const noexcept { return value(); } + explicit operator bool() const noexcept { return ok (); } friend bool operator==(result const &lhs, result const &rhs) noexcept { return lhs.code_ == rhs.code_; } - friend bool operator!=(result const &lhs, result const &rhs) noexcept { return lhs.code_ != rhs.code_; } + friend bool operator!=(result const &lhs, result const &rhs) noexcept { return !(lhs == rhs); } }; /// \brief Custom defined fmt_to method for imp::fmt namespace detail { -inline bool tag_invoke(decltype(::LIBIMP::fmt_to), fmt_context &ctx, result_code r) { - return fmt_to(ctx, "[", (r ? "succ" : "fail"), ", value = ", *r, "]"); -} - template bool tag_invoke(decltype(::LIBIMP::fmt_to), fmt_context &ctx, result r) { - return fmt_to(ctx, "[", (r ? "succ" : "fail"), ", value = ", result::type_traits_t::format(r), "]"); + return fmt_to(ctx, (r ? "succ" : "fail"), ", value = ", result::type_traits_t::format(r)); } } // namespace detail diff --git a/src/libimp/error.cpp b/src/libimp/error.cpp index 12de0f4..fccaeba 100644 --- a/src/libimp/error.cpp +++ b/src/libimp/error.cpp @@ -17,7 +17,13 @@ public: return "generic"; } std::string message(error_code_t const &r) const { - return fmt(r, ((r == 0) ? ", \"success\"" : ", \"failure\"")); + if (r == error_code_t(-1)) { + return "0, \"failure\""; + } + if (r == error_code_t(0)) { + return "0, \"success\""; + } + return fmt(r, ", \"failure\""); } }; @@ -31,6 +37,9 @@ error_category const &generic_category() noexcept { error_code::error_code() noexcept : error_code{0, generic_category()} {} +error_code::error_code(error_code_t const &r) noexcept + : error_code{r, generic_category()} {} + error_code::error_code(error_code_t const &r, error_category const &ec) noexcept : e_code_{r}, ec_{&ec} {} diff --git a/src/libimp/fmt.cpp b/src/libimp/fmt.cpp index 7252591..8ba3ff9 100644 --- a/src/libimp/fmt.cpp +++ b/src/libimp/fmt.cpp @@ -24,10 +24,6 @@ struct sfmt_policy { constexpr static std::size_t aligned_size = 32U; }; -struct sbuf_policy { - constexpr static std::size_t aligned_size = 2048U; -}; - template span local_fmt_sbuf() noexcept { thread_local std::array sbuf; @@ -143,10 +139,6 @@ bool sprintf(fmt_context &ctx, F fop, span const &fstr, span fmt_context_sbuf() noexcept { - return local_fmt_sbuf(); -} - } // namespace /// \brief The context of fmt. @@ -156,9 +148,7 @@ fmt_context::fmt_context(std::string &j) noexcept , offset_(0) {} std::size_t fmt_context::capacity() noexcept { - return (offset_ < fmt_context_sbuf().size()) - ? fmt_context_sbuf().size() - : joined_.size(); + return (offset_ < sbuf_.size()) ? sbuf_.size() : joined_.size(); } void fmt_context::reset() noexcept { @@ -167,8 +157,8 @@ void fmt_context::reset() noexcept { bool fmt_context::finish() noexcept { LIBIMP_TRY { - if (offset_ < fmt_context_sbuf().size()) { - joined_.assign(fmt_context_sbuf().data(), offset_); + if (offset_ < sbuf_.size()) { + joined_.assign(sbuf_.data(), offset_); } else { joined_.resize(offset_); } @@ -183,7 +173,7 @@ span fmt_context::buffer(std::size_t sz) noexcept { constexpr std::size_t fmt_context_aligned_size = 512U; return (sz & ~(fmt_context_aligned_size - 1)) + fmt_context_aligned_size; }; - auto sbuf = fmt_context_sbuf(); + auto sbuf = make_span(sbuf_); LIBIMP_TRY { if (offset_ < sbuf.size()) { if ((offset_ + sz) < sbuf.size()) { diff --git a/src/libimp/result.cpp b/src/libimp/result.cpp deleted file mode 100644 index 55a9941..0000000 --- a/src/libimp/result.cpp +++ /dev/null @@ -1,36 +0,0 @@ - -#include "libimp/result.h" -#include "libimp/horrible_cast.h" - -LIBIMP_NAMESPACE_BEG_ - -result_code::result_code() noexcept - : result_code(false, {}) {} - -result_code::result_code(result_code_t value) noexcept - : result_code(true, value) {} - -result_code::result_code(result_type const &value) noexcept - : result_code(std::get(value), - std::get(value)) {} - -result_code::result_code(bool ok, std::uint64_t code) noexcept - : status_(code, ok) {} - -result_code_t result_code::value() const noexcept { - return std::get(status_); -} - -bool result_code::ok() const noexcept { - return std::get(status_); -} - -bool operator==(result_code const &lhs, result_code const &rhs) noexcept { - return lhs.status_ == rhs.status_; -} - -bool operator!=(result_code const &lhs, result_code const &rhs) noexcept { - return lhs.status_ != rhs.status_; -} - -LIBIMP_NAMESPACE_END_ diff --git a/src/libipc/platform/posix/shm_impl.h b/src/libipc/platform/posix/shm_impl.h index cd3b43a..2d65c47 100644 --- a/src/libipc/platform/posix/shm_impl.h +++ b/src/libipc/platform/posix/shm_impl.h @@ -74,9 +74,17 @@ result shm_open_fd(std::string const &name, mode::type type) noexcept { } /// \brief Create/Open POSIX shared memory bject - return ::shm_open(name.c_str(), flag, S_IRUSR | S_IWUSR | - S_IRGRP | S_IWGRP | - S_IROTH | S_IWOTH); + int fd = ::shm_open(name.c_str(), flag, S_IRUSR | S_IWUSR | + S_IRGRP | S_IWGRP | + S_IROTH | S_IWOTH); + if (fd == posix::failed) { + auto err = sys::error(); + log.error("failed: shm_open(name = ", name, + ", type = ", type, + "). error = ", err); + return err; + } + return fd; } result_code ftruncate_fd(int fd, std::size_t size) noexcept { @@ -85,9 +93,9 @@ result_code ftruncate_fd(int fd, std::size_t size) noexcept { if (::ftruncate(fd, size) != posix::succ) { auto err = sys::error(); log.error("failed: ftruncate(", fd, ", ", size, "). error = ", err); - return err.code(); + return err; } - return {posix::succ}; + return posix::succ; } } // namespace @@ -100,12 +108,8 @@ result_code ftruncate_fd(int fd, std::size_t size) noexcept { result shm_open(std::string name, std::size_t size, mode::type type) noexcept { LIBIMP_LOG_(); auto fd = shm_open_fd(name, type); - if (!fd) return {}; - if (*fd == posix::failed) { - log.error("failed: shm_open(name = ", name, - ", type = ", type, - "). error = ", sys::error()); - return {}; + if (!fd) { + return fd.error(); } LIBIMP_UNUSED auto guard = std::unique_ptr { &fd, [](decltype(fd) *pfd) { @@ -115,26 +119,30 @@ result shm_open(std::string name, std::size_t size, mode::type type) noex /// \brief Try to get the size of this fd struct stat st; if (::fstat(*fd, &st) == posix::failed) { - log.error("failed: fstat(fd = ", *fd, "). error = ", sys::error()); - return {}; + auto err = sys::error(); + log.error("failed: fstat(fd = ", *fd, "). error = ", err); + return err; } /// \brief Truncate this fd to a specified length if (size == 0) { size = static_cast(st.st_size); - if (!ftruncate_fd(*fd, size)) return {}; + auto ret = ftruncate_fd(*fd, size); + if (!ret) return ret.error(); } else if (st.st_size > 0) { /// \remark Based on the actual size. size = static_cast(st.st_size); } else { // st.st_size <= 0 - if (!ftruncate_fd(*fd, size)) return {}; + auto ret = ftruncate_fd(*fd, size); + if (!ret) return ret.error(); } /// \brief Creates a new mapping in the virtual address space of the calling process. void *mem = ::mmap(nullptr, size, PROT_READ | PROT_WRITE, MAP_SHARED, *fd, 0); if (mem == MAP_FAILED) { - log.error("failed: mmap(size = ", size, ", fd = ", *fd, "). error = ", sys::error()); - return {}; + auto err = sys::error(); + log.error("failed: mmap(size = ", size, ", fd = ", *fd, "). error = ", err); + return err; } return new shm_handle{std::move(name), size, mem}; } @@ -149,11 +157,11 @@ result_code shm_close(shm_t h) noexcept { if (::munmap(shm->memp, shm->f_sz) == posix::failed) { auto err = sys::error(); log.error("failed: munmap(", shm->memp, ", ", shm->f_sz, "). error = ", err); - return err.code(); + return err; } /// \brief no unlink the file. delete shm; - return {posix::succ}; + return posix::succ; } LIBIPC_NAMESPACE_END_ diff --git a/src/libipc/platform/win/mmap_impl.h b/src/libipc/platform/win/mmap_impl.h index 6466b41..e1db442 100644 --- a/src/libipc/platform/win/mmap_impl.h +++ b/src/libipc/platform/win/mmap_impl.h @@ -42,9 +42,9 @@ result_code mmap_close(HANDLE h) { if (!::CloseHandle(h)) { auto err = sys::error(); log.error("failed: CloseHandle(", h, "). error = ", err); - return err.code(); + return err; } - return {ERROR_SUCCESS}; + return ERROR_SUCCESS; } /** @@ -78,7 +78,7 @@ result mmap_open(std::string const &file, std::size_t size, mode::type t if (h == NULL) { auto err = sys::error(); log.error("failed: OpenFileMapping(FILE_MAP_ALL_ACCESS, FALSE, ", file, "). error = ", err); - return {nullptr, err.code()}; + return {nullptr, err}; } return h; }; @@ -91,7 +91,7 @@ result mmap_open(std::string const &file, std::size_t size, mode::type t if (h == NULL) { auto err = sys::error(); log.error("failed: CreateFileMapping(PAGE_READWRITE | SEC_COMMIT, ", size, ", ", file, "). error = ", err); - return {nullptr, err.code()}; + return {nullptr, err}; } return h; }; @@ -133,7 +133,7 @@ result mmap_memof(HANDLE h) { if (mem == NULL) { auto err = sys::error(); log.error("failed: MapViewOfFile(", h, ", FILE_MAP_ALL_ACCESS). error = ", err); - return {nullptr, err.code()}; + return {nullptr, err}; } return mem; } @@ -152,7 +152,7 @@ result mmap_sizeof(LPCVOID mem) { if (::VirtualQuery(mem, &mem_info, sizeof(mem_info)) == 0) { auto err = sys::error(); log.error("failed: VirtualQuery(", mem, "). error = ", err); - return {false, (SIZE_T)err.code()}; + return {false, err}; } return mem_info.RegionSize; } diff --git a/src/libipc/platform/win/shm_impl.h b/src/libipc/platform/win/shm_impl.h index 9ff5dea..537b984 100644 --- a/src/libipc/platform/win/shm_impl.h +++ b/src/libipc/platform/win/shm_impl.h @@ -22,19 +22,19 @@ result shm_open(std::string name, std::size_t size, mode::type type) noex auto h = mmap_open(name, size, type); if (*h == NULL) { log.error("failed: mmap_open(name = ", name, ", size = ", size, ", type = ", type, ")."); - return {nullptr, h.code_value()}; + return {nullptr, h.error()}; } auto mem = mmap_memof(*h); if (*mem == NULL) { log.error("failed: mmap_memof(", *h, ")."); mmap_close(*h); - return {nullptr, mem.code_value()}; + return {nullptr, mem.error()}; } auto sz = mmap_sizeof(*mem); if (!sz) { log.error("failed: mmap_sizeof(", *mem, ")."); mmap_close(*h); - return {nullptr, static_cast(sz.value())}; + return {nullptr, sz.error()}; } return new shm_handle{std::move(name), *sz, *mem, *h}; } diff --git a/test/imp/test_imp_result.cpp b/test/imp/test_imp_result.cpp index 757038e..5f5bf89 100644 --- a/test/imp/test_imp_result.cpp +++ b/test/imp/test_imp_result.cpp @@ -13,50 +13,58 @@ TEST(result, ok) { EXPECT_FALSE(ret.ok()); EXPECT_EQ(ret.value(), 0); - ret = {true, 0}; + ret = {0, true}; EXPECT_TRUE(ret); EXPECT_TRUE(ret.ok()); EXPECT_EQ(ret.value(), 0); - ret = {false, 1234}; + ret = {0, false}; + EXPECT_FALSE(ret); + EXPECT_FALSE(ret.ok()); + EXPECT_EQ(ret.value(), 0); + + ret = {0}; + EXPECT_TRUE(ret); + EXPECT_TRUE(ret.ok()); + EXPECT_EQ(ret.value(), 0); + + ret = {1234, false}; EXPECT_FALSE(ret); EXPECT_FALSE(ret.ok()); EXPECT_EQ(*ret, 1234); + ret = imp::result_code(1234, true); + EXPECT_TRUE(ret); + EXPECT_TRUE(ret.ok()); + EXPECT_EQ(ret.value(), 1234); + + ret = {0, false}; + EXPECT_FALSE(ret); + EXPECT_FALSE(ret.ok()); + EXPECT_EQ(ret.value(), 0); + + ret = {4321, true}; + EXPECT_TRUE(ret); + EXPECT_TRUE(ret.ok()); + EXPECT_EQ(ret.value(), 4321); + imp::result r2 {nullptr, 4321}; EXPECT_NE(r2, nullptr); // imp::result{nullptr} EXPECT_EQ(*r2, nullptr); EXPECT_FALSE(r2); } -TEST(result, code) { - imp::result_code ret(true, 1234); - EXPECT_TRUE(ret); - EXPECT_TRUE(ret.ok()); - EXPECT_EQ(ret.value(), 1234); - - ret = {false, 0}; - EXPECT_FALSE(ret); - EXPECT_FALSE(ret.ok()); - EXPECT_EQ(ret.value(), 0); - - ret = {true, 4321}; - EXPECT_TRUE(ret); - EXPECT_TRUE(ret.ok()); - EXPECT_EQ(ret.value(), 4321); -} - TEST(result, compare) { imp::result_code r1, r2; EXPECT_EQ(r1, r2); - imp::result_code r3(true); + imp::result_code r3(0, true); EXPECT_NE(r1, r3); - imp::result_code r4(true, 222222); + imp::result_code r4(222222, true); EXPECT_NE(r3, r4); - imp::result_code r5(false, 222222); + imp::result_code r5(222222, false); EXPECT_NE(r4, r5); r3 = r5; EXPECT_EQ(r3, r5); @@ -65,33 +73,33 @@ TEST(result, compare) { TEST(result, fmt) { { imp::result_code r1; - EXPECT_EQ(imp::fmt(r1), "[fail, value = 0]"); - imp::result_code r2(true, 65537); - EXPECT_EQ(imp::fmt(r2), "[succ, value = 65537]"); + EXPECT_EQ(imp::fmt(r1), "fail, value = 0"); + imp::result_code r2(65537, true); + EXPECT_EQ(imp::fmt(r2), "succ, value = 65537"); imp::result_code r3(0); - EXPECT_EQ(imp::fmt(r3), "[succ, value = 0]"); + EXPECT_EQ(imp::fmt(r3), "succ, value = 0"); } { imp::result r0; EXPECT_EQ(imp::fmt(r0), imp::fmt(imp::result_code())); - imp::result r1 {false, -123}; - EXPECT_EQ(imp::fmt(r1), imp::fmt("[fail, value = ", -123, "]")); + imp::result r1 {-123, false}; + EXPECT_EQ(imp::fmt(r1), imp::fmt("fail, value = ", -123)); imp::result r2 {&r1}; - EXPECT_EQ(imp::fmt(r2), imp::fmt("[succ, value = ", (void *)&r1, "]")); + EXPECT_EQ(imp::fmt(r2), imp::fmt("succ, value = ", (void *)&r1)); int aaa {}; imp::result r3 {&aaa}; - EXPECT_EQ(imp::fmt(r3), imp::fmt("[succ, value = ", (void *)&aaa, "]")); + EXPECT_EQ(imp::fmt(r3), imp::fmt("succ, value = ", (void *)&aaa)); imp::result r4 {nullptr}; - EXPECT_EQ(imp::fmt(r4), imp::fmt("[fail, value = ", nullptr, ", code = 0]")); + EXPECT_EQ(imp::fmt(r4), imp::fmt("fail, value = ", nullptr, ", error = [generic: 0, \"failure\"]")); r4 = {nullptr, 1234}; - EXPECT_EQ(imp::fmt(r4), imp::fmt("[fail, value = ", nullptr, ", code = 1234]")); + EXPECT_EQ(imp::fmt(r4), imp::fmt("fail, value = ", nullptr, ", error = [generic: 1234, \"failure\"]")); imp::result r5; - EXPECT_EQ(imp::fmt(r5), "[fail, value = null, code = 0]"); + EXPECT_EQ(imp::fmt(r5), "fail, value = null, error = [generic: 0, \"failure\"]"); } { imp::result r1 {-123}; - EXPECT_EQ(imp::fmt(r1), imp::fmt("[succ, value = ", -123, "]")); + EXPECT_EQ(imp::fmt(r1), imp::fmt("succ, value = ", -123)); } } \ No newline at end of file