diff --git a/include/CLI/TypeTools.hpp b/include/CLI/TypeTools.hpp index 48f65bfd2..780bd2f44 100644 --- a/include/CLI/TypeTools.hpp +++ b/include/CLI/TypeTools.hpp @@ -87,6 +87,23 @@ template <> struct IsMemberType { using type = std::string; }; +namespace adl_detail { +/// Check for existence of user-supplied lexical_cast. +/// +/// This struct has to be in a separate namespace so that it doesn't see our lexical_cast overloads in CLI::detail. +/// Standard says it shouldn't see them if it's defined before the corresponding lexical_cast declarations, but this +/// requires a working implementation of two-phase lookup, and not all compilers can boast that (msvc, ahem). +template class is_lexical_castable { + template + static auto test(int) -> decltype(lexical_cast(std::declval(), std::declval()), std::true_type()); + + template static auto test(...) -> std::false_type; + + public: + static constexpr bool value = decltype(test(0))::value; +}; +} // namespace adl_detail + namespace detail { // These are utilities for IsMember and other transforming objects @@ -1247,13 +1264,24 @@ bool lexical_cast(const std::string &input, T &output) { /// Non-string parsable by a stream template ::value == object_category::other && !std::is_assignable::value, + enable_if_t::value == object_category::other && !std::is_assignable::value && + is_istreamable::value, detail::enabler> = detail::dummy> bool lexical_cast(const std::string &input, T &output) { - static_assert(is_istreamable::value, + return from_stream(input, output); +} + +/// Fallback overload that prints a human-readable error for types that we don't recognize and that don't have a +/// user-supplied lexical_cast overload. +template ::value == object_category::other && !std::is_assignable::value && + !is_istreamable::value && !adl_detail::is_lexical_castable::value, + detail::enabler> = detail::dummy> +bool lexical_cast(const std::string & /*input*/, T & /*output*/) { + static_assert(!std::is_same::value, // Can't just write false here. "option object type must have a lexical cast overload or streaming input operator(>>) defined, if it " "is convertible from another type use the add_option(...) with XC being the known type"); - return from_stream(input, output); + return false; } /// Assign a value through lexical cast operations diff --git a/tests/NewParseTest.cpp b/tests/NewParseTest.cpp index 9f5aea20a..18e68e85b 100644 --- a/tests/NewParseTest.cpp +++ b/tests/NewParseTest.cpp @@ -283,6 +283,33 @@ TEST_CASE_METHOD(TApp, "custom_string_converter_specialize", "[newparse]") { CHECK("something!" == s.s); } +/// Yet another wrapper to test that overloading lexical_cast with enable_if works. +struct yetanotherstring { + yetanotherstring() = default; + std::string s{}; +}; + +template struct is_my_lexical_cast_enabled : std::false_type {}; + +template <> struct is_my_lexical_cast_enabled : std::true_type {}; + +template ::value, CLI::detail::enabler> = CLI::detail::dummy> +bool lexical_cast(const std::string &input, T &output) { + output.s = input; + return true; +} + +TEST_CASE_METHOD(TApp, "custom_string_converter_adl_enable_if", "[newparse]") { + yetanotherstring s; + + app.add_option("-s", s); + + args = {"-s", "something"}; + + run(); + CHECK("something" == s.s); +} + /// simple class to wrap another with a very specific type constructor and assignment operators to test out some of the /// option assignments template class objWrapper {