Skip to content

Commit

Permalink
Can add lexical_cast overloads constrained with enable_if (#1021)
Browse files Browse the repository at this point in the history
Also works for overloads constrained with concepts or requirements.

Previously this wasn't working, which is a problem if you want to hook
some external serialization framework into CLI11 without painstakingly
going through all the types that said external framework supports.

This PR is related to #908.
  • Loading branch information
captainurist committed May 2, 2024
1 parent 6d83f45 commit 2e8697c
Show file tree
Hide file tree
Showing 2 changed files with 58 additions and 3 deletions.
34 changes: 31 additions & 3 deletions include/CLI/TypeTools.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,23 @@ template <> struct IsMemberType<const char *> {
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 <typename T, typename S = std::string> class is_lexical_castable {
template <typename TT, typename SS>
static auto test(int) -> decltype(lexical_cast(std::declval<const SS &>(), std::declval<TT &>()), std::true_type());

template <typename, typename> static auto test(...) -> std::false_type;

public:
static constexpr bool value = decltype(test<T, S>(0))::value;
};
} // namespace adl_detail

namespace detail {

// These are utilities for IsMember and other transforming objects
Expand Down Expand Up @@ -1247,13 +1264,24 @@ bool lexical_cast(const std::string &input, T &output) {

/// Non-string parsable by a stream
template <typename T,
enable_if_t<classify_object<T>::value == object_category::other && !std::is_assignable<T &, int>::value,
enable_if_t<classify_object<T>::value == object_category::other && !std::is_assignable<T &, int>::value &&
is_istreamable<T>::value,
detail::enabler> = detail::dummy>
bool lexical_cast(const std::string &input, T &output) {
static_assert(is_istreamable<T>::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 <typename T,
enable_if_t<classify_object<T>::value == object_category::other && !std::is_assignable<T &, int>::value &&
!is_istreamable<T>::value && !adl_detail::is_lexical_castable<T>::value,
detail::enabler> = detail::dummy>
bool lexical_cast(const std::string & /*input*/, T & /*output*/) {
static_assert(!std::is_same<T, T>::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<T, XC>(...) with XC being the known type");
return from_stream(input, output);
return false;
}

/// Assign a value through lexical cast operations
Expand Down
27 changes: 27 additions & 0 deletions tests/NewParseTest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 <class T> struct is_my_lexical_cast_enabled : std::false_type {};

template <> struct is_my_lexical_cast_enabled<yetanotherstring> : std::true_type {};

template <class T, CLI::enable_if_t<is_my_lexical_cast_enabled<T>::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 X> class objWrapper {
Expand Down

0 comments on commit 2e8697c

Please sign in to comment.