1

I'm playing around with C++ concepts and came across an interesting problem. I have the following two custom-defined concepts:

template<typename T>
concept is_dereferencable = requires (T t) { *t; };

template<typename T>
concept is_printable = requires (T t) { std::cout << t; };

As the names suggest, the first one is used to determine if a given type can be dereferenced, while the other one to check if a type supports output operator. I also have a function template called println, which looks like this:

template<typename T>
void println(const T& t)
{
    if constexpr (is_dereferencable<T>) {
        if constexpr (is_printable<decltype(*t)>) {
            std::cout << *t << '\n';
        }
    } else if constexpr (is_printable<T>) {
        std::cout << t << '\n';
    }
}

This prints the dereferenced value *t if and only if type T is dereference-able and the type of the dereferenced value is printable. So, for example, I can use this function template with something like an std::optional:

int main
{
    std::optional<std::string> stringOpt {"My Optional String"};
    ::println(stringOpt);

    return 0;
}

This will print My Optional String as expected. While this is nice, the function will just silently print nothing if the type of a dereferenced value of a derefernce-able is not printable. So, for a user-defined type Person, the following will just print nothing:

struct Person
{
    std::string m_name;
    explicit Person(const std::string& name) : m_name {name} {}
};

int main
{
    std::optional<Person> personOpt {"John Doe"}
    ::println(personOpt);
    return 0;
}

So I would like to move the above compile-time ifs to a requires clause itself in order to get compile time errors in such cases. Is there a way to achieve that? Is there a way to get the dereferenced type of a given template type T? To make it a bit clearer, I would like to have something like this:

template<typename T>
requires is_dereferencable<T> && is_printable<decltype(*T)>
void printDereferencable(const T& t)
{
    std::cout << *t << '\n';
}

P.S.: I understand that I could remove the nested if and just fail upon trying to call an output operator on something that doesn't support it. However, I want to specifically move this compile-time error to the concept to get a clearer error message.

Hidayat Rzayev
  • 339
  • 3
  • 14
  • Why not `if constexpr (is_dereferencable) { static_assert(is_printable, "nice message here"); std::cout << *t << '\n'; }`? – chi Jan 30 '23 at 22:35
  • You take a `T const&` and call `operator *()` on it. No dereference [needs to] happen. It's just a member function call. (Which might happen to work on optional, and smart pointers. You're better off making overload of a non-templated function, in my experience) EDIT: phrasing. – viraltaco_ Jan 30 '23 at 22:36
  • @chi This would work pretty well, good point. I would still like to know if something like that would be possible though. – Hidayat Rzayev Jan 30 '23 at 22:42
  • @viraltaco_ It also works on raw pointers and iterators. Regardless, that's not the point and is not so relevant for the question. – Hidayat Rzayev Jan 30 '23 at 22:43
  • @hidayat-rzayev I'm not sure if it's relevant, because I'm not sure it's valid given [temp.constr.atomic-3](https://eel.is/c++draft/temp.constr.constr#temp.constr.atomic-3) since it's not a constant expression. `std::cout` is a global variable, not a class. – viraltaco_ Jan 30 '23 at 23:13

2 Answers2

2

you can use std::declval

template<typename T>
requires is_dereferencable<const T&> && is_printable<decltype(*std::declval<const T&>())>
void printDereferencable(const T& t)
{
    std::cout << *t << '\n';
}

or you can just write a is_dereference_printable concept

template<typename T>
concept is_dereference_printable = requires (T t) { std::cout << *t; };
apple apple
  • 10,292
  • 2
  • 16
  • 36
  • Exactly what I was looking for! Great to know about `std::declval` :) Thank you! – Hidayat Rzayev Jan 30 '23 at 22:50
  • 1
    @HidayatRzayev `std::declval()` is needed to support non-default-constructible types. Otherwise you might use just `decltype(*T())`. – Evg Jan 30 '23 at 22:53
  • 1
    Technically, `std::declval()` is used incorrectly here. `*t` in `std::cout << *t` is called on `const T`, but in `*declval()` it's called on `T`. For some [weird types](https://godbolt.org/z/Parh1hcxP), these could be completely different operators. – Evg Jan 30 '23 at 23:01
  • @Evg good point, it may need to be cast to `const T&` – apple apple Jan 30 '23 at 23:06
  • 1
    `*std::declval()` should work. In generic code we should also take into account possibility to overload on `&` and `&&` ref qualifiers. – Evg Jan 30 '23 at 23:06
  • 1
    @Evg and it should also apply to all constraint parameter, or it would fail for some other weird type https://godbolt.org/z/vx1WG8aqY – apple apple Jan 30 '23 at 23:12
  • fwiw, I was under the impression `declval` return a value, well obviously it's not the case here. – apple apple Jan 30 '23 at 23:21
  • It always returns a some kind of a reference - to return a value you have to construct it, which you can't do for non-default-constructible types. – Evg Jan 30 '23 at 23:24
  • @Evg I believe unevaluated function (like usage of `declval`) doesn't have that constraint. – apple apple Jan 30 '23 at 23:31
  • Even in unevaluated context like `decltype` you [can't](https://godbolt.org/z/evPjvrG93) default-construct a non-default-constructible type. The main purpose of `std::declval()`, which returns a reference instead of a value, is to [solve](https://godbolt.org/z/TYvjGeWr8) that problem. – Evg Jan 30 '23 at 23:52
  • 1
    @Evg that's true, well, but my point is `declval` *can* be declared to return value (or `T`) https://godbolt.org/z/99f39q4s4 – apple apple Jan 31 '23 at 13:44
  • 1
    For my example, that's true. But for general `T` we [can't](https://stackoverflow.com/questions/25707441/why-does-stddeclval-add-a-reference) do it. – Evg Jan 31 '23 at 15:11
2

another option is put the constraint after parameters, so it has access to them.

template<typename T>
void printDereferencable(const T& t)
requires is_dereferencable<const T&> && is_printable<decltype(*t)>
{
    std::cout << *t << '\n';
}

note: it should test on const T&, not T

apple apple
  • 10,292
  • 2
  • 16
  • 36