47

I'm trying to write syntactic sugar, in a monad-style, over std::optional. Please consider:

template<class T>
void f(std::optional<T>)
{}

As is, this function cannot be called with a non-optional T1 (e.g. an int), even though there exists a conversion from T to std::optional<T>2.

Is there a way to make f accept an std::optional<T> or a T (converted to an optional at the caller site), without defining an overload3?


1) f(0): error: no matching function for call to 'f(int)' and note: template argument deduction/substitution failed, (demo).
2) Because template argument deduction doesn't consider conversions.
3) Overloading is an acceptable solution for a unary function, but starts to be an annoyance when you have binary functions like operator+(optional, optional), and is a pain for ternary, 4-ary, etc. functions.

YSC
  • 38,212
  • 9
  • 96
  • 149
  • 13
    have it take a T and then just check if it's an optional and if it's not, make it one. constexpr if can do that super easy if you're using c++17. – xaxxon Aug 21 '18 at 09:00
  • 8
    @xaxxon that's worth an answer – YSC Aug 21 '18 at 09:01
  • 3
    Can you clarify what you mean by “in a monad-style”? Because as it stands your question suggests you are interested in some kind of automatic conversion or overload, and both answers do something along these lines. **But this has nothing whatsoever to do with monads**. – Konrad Rudolph Aug 21 '18 at 10:43
  • @KonradRudolph The mention of monads was just to give a context. I want, provided `F=λxy.`, an `optional(x)` and an `optional(y)`, return an `optional(F(xy))`. For such a definition to be useful and let its user chain expressions, it needs to accept both optionals and scalars. – YSC Aug 21 '18 at 11:39
  • The problem with this definition is that, unlike a monad, *it does not compose*. How would your function handle a `std::optional>`? Whereas if you had a monad lifting operator you would simply `lift(F)` and wouldn’t have this problem. – Konrad Rudolph Aug 21 '18 at 12:49
  • @KonradRudolph Yes. That's step two. I'm better fixing a problem at a time ;) – YSC Aug 21 '18 at 13:03
  • @YSC But if you have `lift` you don’t need this overload. – Konrad Rudolph Aug 21 '18 at 13:03
  • @KonradRudolph OK, I get it. I'm not sure I want to mimic haskel in all aspects though. – YSC Aug 21 '18 at 13:06

4 Answers4

39

Another version. This one doesn't involve anything:

template <typename T>
void f(T&& t) {
    std::optional opt = std::forward<T>(t);
}

Class template argument deduction already does the right thing here. If t is an optional, the copy deduction candidate will be preferred and we get the same type back. Otherwise, we wrap it.

Barry
  • 286,269
  • 29
  • 621
  • 977
15

Instead of taking optional as argument take deductible template parameter:

template<class T>
struct is_optional : std::false_type{};

template<class T>
struct is_optional<std::optional<T>> : std::true_type{};

template<class T, class = std::enable_if_t<is_optional<std::decay_t<T>>::value>>
constexpr decltype(auto) to_optional(T &&val){
    return std::forward<T>(val);
}

template<class T, class = std::enable_if_t<!is_optional<std::decay_t<T>>::value>>
constexpr std::optional<std::decay_t<T>> to_optional(T &&val){
    return { std::forward<T>(val) };
}

template<class T>
void f(T &&t){
    auto opt = to_optional(std::forward<T>(t));
}

int main() {
    f(1);
    f(std::optional<int>(1));
}

Live example

bartop
  • 9,971
  • 1
  • 23
  • 54
14

This uses one of my favorite type traits, which can check any all-type template against a type to see if it's the template for it.

#include <iostream>
#include <type_traits>
#include <optional>


template<template<class...> class tmpl, typename T>
struct x_is_template_for : public std::false_type {};

template<template<class...> class tmpl, class... Args>
struct x_is_template_for<tmpl, tmpl<Args...>> : public std::true_type {};

template<template<class...> class tmpl, typename... Ts>
using is_template_for = std::conjunction<x_is_template_for<tmpl, std::decay_t<Ts>>...>;

template<template<class...> class tmpl, typename... Ts>
constexpr bool is_template_for_v = is_template_for<tmpl, Ts...>::value;


template <typename T>
void f(T && t) {
    auto optional_t = [&]{
        if constexpr (is_template_for_v<std::optional, T>) {
            return t; 
        } else {
            return std::optional<std::remove_reference_t<T>>(std::forward<T>(t));
        }
    }();
    (void)optional_t;
}

int main() {
    int i = 5;
    std::optional<int> oi{5};

    f(i);
    f(oi);
}

https://godbolt.org/z/HXgoEE

YSC
  • 38,212
  • 9
  • 96
  • 149
xaxxon
  • 19,189
  • 5
  • 50
  • 80
  • Really nice trait you've got here. Do you know why `std::decay` is necessary? – YSC Aug 21 '18 at 09:22
  • 2
    @YSC Without `std::decay` it would not work automagically with types like `std::optional &` and related ones – bartop Aug 21 '18 at 09:31
5

Another version. This one doesn't involve writing traits:

template <typename T>
struct make_optional_t {
    template <typename U>
    auto operator()(U&& u) const {
        return std::optional<T>(std::forward<U>(u));
    }
};

template <typename T>
struct make_optional_t<std::optional<T>> {
    template <typename U>
    auto operator()(U&& u) const {
        return std::forward<U>(u);
    }
};

template <typename T>
inline make_optional_t<std::decay_t<T>> make_optional;

template <typename T>
void f(T&& t){
    auto opt = make_optional<T>(std::forward<T>(t));
}
Barry
  • 286,269
  • 29
  • 621
  • 977