1

I'm trying to use std::enable_if_t to enable a class if the template parameter is any type of std::string (bonus points for an answer that shows how to do this with a general string from multiple libraries).

I don't want to have to do "if Text is a const char*, std::string, std::string_view, etc." Essentially, I don't want to specifically mention every possible string-like object. All I'm going to do with this object is print it to console, and the Text object will be stored as a class attribute.

Is there a more elegant way of doing this than accounting for every individual type?

Drake Johnson
  • 640
  • 3
  • 19
  • Do you want to be able to have a function `print` in your class if the member is `printable` (whatever this means)? This would also include `int`s and other primitive types, but for the purpose of printing to console, `int`s seem very much like strings to me – Stefan Groth Jan 24 '20 at 10:06
  • @StefanGroth If you look at the first comment on the other answer by ALX23z, I defined exactly what I meant by printable :) – Drake Johnson Jan 24 '20 at 10:08

2 Answers2

2

Checkout the type_traits library:

https://en.cppreference.com/w/cpp/header/type_traits

You are probably interested in is_constructible, is_convertible, or is_assignable depending on the exact method you want to use.

For example is_constructible<T, Args...> tests if there is a constructor T::T(Args...).

is_convertible<From, To> tests if To can be converted to From: e.g., following code compiles From Y = (From)X; where X is a To

ALX23z
  • 4,456
  • 1
  • 11
  • 18
  • I did look into this and, funny enough, found out about these after I posted the question. Nevertheless, I think that `std::string_view` is the problem with these. Is there a way to deal with a general "printable" type (meaning a type that is not numeric but can be printed to `std::cout`)? – Drake Johnson Jan 24 '20 at 00:23
  • @DrakeJohnson I am fairly certain that `std::string_view` is convertible to `std::string`, no? – ALX23z Jan 24 '20 at 00:27
  • It shows `is_convertible` is false from `string_view` to `string`. However, looking at how the STL does it in the `` header, they define a template typedef (of type `_StringViewIsh`) `using _Is_string_view_ish = enable_if_t>, negation>>>;` The problem is that this is kinda difficult to wrap my head around. – Drake Johnson Jan 24 '20 at 00:31
  • @DrakeJohnson you can do the other thing - `std::string` is convertible to `std::string_view`. So you can easily write function that converts whatever into the `std::string_view` and prints it - or just tries to print it without attempting to convert (at least make specialization for `const char*`). – ALX23z Jan 24 '20 at 00:37
1

You can use is_detected for this. We're trying to check whether the given type is printable with one of std::couts overloads of operator<< or if the type itself provides an operator<< for printing to std::cout. For a more general explanation of how I implemented this, check out https://www.fluentcpp.com/2017/06/02/write-template-metaprogramming-expressively/

First, we define an appropriate is_detected for the overloads of std::cout itself:

// check std::cout.operator<<(T {})
template<typename = void, typename Arg = void> struct test_operator_of_cout : std::false_type {};

template<typename Arg>
struct test_operator_of_cout<std::void_t<decltype(std::cout.operator<<(std::declval<Arg>()))>, Arg>
    : std::true_type {};

template<typename Arg>
constexpr bool test_operator_of_cout_v = test_operator_of_cout<void, Arg>::value;

And another one for all overloads of operator<<(ostream&, T {}). The link I posted above generalizes this to have less code redundancy.

// check operator<<(std::cout, T {})
template<typename = void, typename Arg = void> struct test_operator_of_struct : std::false_type {};

template<typename Arg>
struct test_operator_of_struct<std::void_t<decltype(operator<<(std::cout, std::declval<Arg>()))>, Arg>
    : std::true_type {};

template<typename Arg>
constexpr bool test_operator_of_struct_v = test_operator_of_struct<void, Arg>::value;

We can now use these type traits to implement the print function with enable_if:

template<typename T> struct MyClass {

    T t;

    template<
        typename Consider = T,
        typename = std::enable_if_t<
            ( test_operator_of_cout_v<Consider> || test_operator_of_struct_v<Consider>) 
            && !std::is_arithmetic_v<Consider>
        >
    > void print() {
        std::cout << t;
    }

};

There are two things to note here:

  • You need the first template argument of Consider = T. Otherwise, the compiler will try to instantiate the declaration of the function, which is ill-formed for types that do not fullfil the condition. Check out this SO-Answer for a more in-depth explanation: std::enable_if to conditionally compile a member function
  • Arithmetic types are not printable because of the !std::is_arithmetic. I personally would not include this because I do not see a reason why my class shouldn't allow perfectly printable types to be printable.

Now we can look at what is printable and what not:

struct NotPrintable {};

struct Printable {
    friend std::ostream& operator<<(std::ostream& os, const Printable& p) {
         return os;
    }
};

auto foo() {

    MyClass<const char *> chars;
    chars.print(); //compiles

    MyClass<std::string> strings;
    strings.print(); //compiles

    MyClass<std::string_view> string_views;
    string_views.print(); //compiles

    MyClass<Printable> printables;
    printables.print(); // compiles

    // MyClass<int> ints;
    // ints.print(); // Does not compile due to !is_arithmetiv_v

    // MyClass<NotPrintable> not_printable;
    // not_printable.print(); //Does not compile due to operator checking

}

You can check out the complete example here: https://godbolt.org/z/ZC9__e

Stefan Groth
  • 154
  • 7