2

Consider the following piece of code:

template <typename T>
struct wrap {
    T thing;
    constexpr wrap(T thing) : thing(thing) {}
};
template <typename T>
wrap(const T&) -> wrap<T>;

template <wrap V>
void fun();

struct X {
    int a;
};

int main() {
    constexpr auto a1 = &X::a;
    static const auto a2 = &X::a;
    fun<a1>();
    fun<a2>(); // Doesn't compile
}

Now the thing is that one can successfully pass a1 through wrap without having to state the template parameter explicitly, but not a2 which has static-storage duration.

If I change the above deduction guide to this:

template <typename T>
wrap(const T&) -> wrap<const T&>; // <-- Note the 'const T&' here instead of plain 'T'

Then one is able to pass a2 but not a1.

If possible, how to modify the above code so that one is able to pass both a1 and a2 respectively without having to explicitly state the types like fun<wrap<decltype(a1)>{a1}>() or fun<wrap<const decltype(a2)&>{a2}>()?

lekner
  • 21
  • 1
  • 2
    Is there a reason to want `a2` to work? Even if there are specific cases where you *can* use a non-`constexpr` variable in a constant expression, there's no reason you *should* do so. So just make people use `constexpr` like a normal person. – Nicol Bolas Nov 10 '21 at 19:09

1 Answers1

2

Sure, it's possible. However, before I explain the solution, please allow me to suggest that you have made the problem unnecessarily difficult by insisting on a particular interface which is not (in my opinion) actually cleaner than the alternatives. Essentially, you are asking for fun<arg> to either take arg by value or by reference, depending on which one is actually well-formed. In the case of a1, it may only be taken by value; it cannot be taken by reference because it doesn't have static storage duration. In the case of a2, it cannot be taken by value because it wasn't declared constexpr, but can be taken by reference because it has static storage duration.

The code using your proposed version of fun is difficult to read, because the reader, seeing fun<arg>, does not immediately know whether arg is being taken by value or by reference. The reader must infer which one it is, based on the reader's own knowledge of whether arg is a permitted non-type template parameter by value or by reference. Furthermore, some arguments may qualify as either, and in that case the reader would also have to know which default the implementer of fun has selected for that case, in order to know what is going on.

Again, this is my opinion only: it would be much simpler if you wrote separate functions, perhaps calling them fun_val and fun_ref, where fun_val<a1>() and fun_ref<a2>() are well-formed. For this, we should define two wrapper classes, one which takes the argument by value, and one by reference:

template <typename T>
struct wrap_value {
    using value_type = T;
    T thing;
    constexpr wrap_value(T thing) : thing(thing) {}
};
template <typename T>
wrap_value(const T&) -> wrap_value<T>;

template <typename T>
struct wrap_reference {
    using value_type = T;
    const T& thing;
    constexpr wrap_reference(const T& thing) : thing(thing) {}
};
template <typename T>
wrap_reference(const T&) -> wrap_reference<T>;

template <wrap_value V>
void fun_val() {
    std::cout << "value\n";
}

template <wrap_reference V>
void fun_ref() {
    std::cout << "reference\n";
}

struct X {
    int a;
};

int main() {
    constexpr auto a1 = &X::a;
    static const auto a2 = &X::a;
    static const int x = 42;
    fun_val<a1>();  // OK
    fun_ref<a1>();  // Error
    fun_val<a2>();  // Error
    fun_ref<a2>();  // OK
    fun_val<x>();   // OK; uses value of x
    fun_ref<x>();   // OK; uses address of x
}

Now, if you insist on having a single name fun, then the key is to recognize that a1 and a2 have the same type, so a single application of CTAD will never be able to figure out the correct wrapper type to make the invocation well-formed. Instead, you have to use SFINAE with two overloads: the one that is invalid for the given template argument (because it takes the argument by value (resp. reference) that cannot be taken by value (resp. reference)) is discarded. Basically, rename both fun_val and fun_ref in the above example to simply fun:

template <wrap_value V>
void fun() {
    std::cout << "value\n";
}

template <wrap_reference V>
void fun() {
    std::cout << "reference\n";
}

This works fine in the case of a1 and a2, for which only one of the two overloads is a candidate. But in the case of x, it will be ambiguous. Let's say you want to force the by-value overload to be selected in that case. We can do that by inserting a constraint that makes the by-reference overload not a candidate:

template <wrap_reference V> requires(!requires { fun<wrap_value<typename decltype(V)::value_type>(V.thing)>(); })
void fun() {
    std::cout << "reference\n";
}

You can view the full working example here.

Brian Bi
  • 111,498
  • 10
  • 176
  • 312