Std::Optional Specialization for Reference Types

std::optional specialization for reference types

When n3406 (revision #2 of the proposal) was discussed, some committee members were uncomfortable with optional references. In n3527 (revision #3), the authors decided to make optional references an auxiliary proposal, to increase the chances of getting optional values approved and put into what became C++14. While optional didn't quite make it into C++14 for various other reasons, the committee did not reject optional references and is free to add optional references in the future should someone propose it.

Why GCC rejects std::optional for references?

Because optional, as standardized in C++17, does not permit reference types. This was excluded by design.

There are two reasons for this. The first is that, structurally speaking, an optional<T&> is equivalent to a T*. They may have different interfaces, but they do the same thing.

The second thing is that there was effectively no consensus by the standards committee on questions of exactly how optional<T&> should behave.

Consider the following:

optional<T&> ot = ...;
T t = ...;
ot = t;

What should that last line do? Is it taking the object being referenced by ot and copy-assign to it, such that *ot == t? Or should it rebind the stored reference itself, such that ot.get() == &t? Worse, will it do different things based on whether ot was engaged or not before the assignment?

Some people will expect it to do one thing, and some people will expect it to do the other. So no matter which side you pick, somebody is going to be confused.

If you had used a T* instead, it would be quite clear which happens:

T* pt = ...;
T t = ...;
pt = t; //Compile error. Be more specific.
*pt = t; //Assign to pointed-to object.
pt = &t; //Change pointer.

Specializing std::optional

The general rule in 17.6.4.2.1 [namespace.std]/1 applies:

A program may add a template specialization for any standard library template to namespace std only if the declaration depends on a user-defined type and the specialization meets the standard library requirements for the original template and is not explicitly
prohibited.

So I would say it's allowed.

N.B. optional will not be part of the C++14 standard, it will be included in a separate Technical Specification on library fundamentals, so there is time to change the rule if my interpretation is wrong.

How to make a class template specialization for reference and non-reference types?

So I found a way that suits my needs and that is basically the combination of @Kevin's answer and @Sam Varshavchik's comment on the question. So I'm using std::shared_ptr with Class template argument deduction guides. Here's my solution:

template<typename> // Base Condition 
class Drawable {};

/* Lvalue */
template<>
class Drawable<Pen &> {
Pen &m_pen;

public:
Drawable(Pen &a)
:m_pen(a) {}
};

/* Rvalue */
template<>
class Drawable<Pen &&> {
Pen m_pen;

public:
Drawable(Pen &&a)
:m_pen(std::move(a)) {}
};

/* Shared */
template<>
class Drawable<std::shared_ptr<Pen>> {
std::shared_ptr<Pen> m_pen;

public:
Drawable(std::shared_ptr<Pen> pen)
:m_pen(pen) {}
};

/* These guides help out the compiler determine the right specialization */
Drawable(Pen &) -> Drawable<Pen &>;
Drawable(Pen &&) -> Drawable<Pen &&>;
Drawable(std::shared_ptr<Pen>) -> Drawable<std::shared_ptr<Pen>>;

All this is, is 3 specializations for the 3 cases I want to support and guides to help the compiler decide which 1 the compiler should pick. Now the user can pass in a existing or temporary Pen or a existing or temporary std::shared_ptr to a Pen and everything just works without the user having to specify any template arguments.

To remove repeating code just move it into a base class and then inherit from it in all 3 cases.

By the way that template argument doesn't have to exactly match what you're passing in. I just made it that way to make the code more readable (ig). You can also get away by doing something like this.

template<int>
class Drawable {};

/* Lvalue */
template<>
class Drawable<1> {
Pen &m_pen;

public:
Drawable(Pen &a)
:m_pen(a) {}

Pen &get() { return m_pen; }
};

/* Rvalue */
template<>
class Drawable<2> {
Pen m_pen;

public:
Drawable(Pen &&a)
:m_pen(std::move(a)) {}

Pen &get() { return m_pen; }
};

/* Shared */
template<>
class Drawable<3> {
std::shared_ptr<Pen> m_pen;

public:
Drawable(std::shared_ptr<Pen> pen)
:m_pen(pen) {}

std::shared_ptr<Pen> &get() { return m_pen; }
};

Drawable(Pen &) -> Drawable<1>;
Drawable(Pen &&) -> Drawable<2>;
Drawable(std::shared_ptr<Pen>) -> Drawable<3>;

Here instead of using types are template parameters I'm using numbers also don't use this method as you don't understand whats going on. This was just an example to demonstrate a point.

So now you main() can look something like this:

int main() {
Pen ref(69);

Drawable a(Pen(69));
Drawable b(ref);
Drawable c(std::make_shared<Pen>(33));

ref = 420; // Now the Pen inside Drawable is also 420
}

Thanks for the help @Kevin, @Sam Varshavchik and @Aconcagua

Use of std::optional to pass a std::vector int to a functional by reference

From what I'm aware, this isn't possible in the standard as one hasn't agreed upon the effects of an assignment.

What you want to achieve is possible with a library feature:

  void test_func(std::optional<std::reference_wrapper<std::vector<int>>> vec)

How to specialize classes for all reference types C++03

As seen (correctly) in this thread Specializing function template for reference types, the remove_reference suggested here won't work. It just won't go into the second implementation EVER, because the compiler sees T& and T just the same.

Instead you could MANUALLY tell the compiler that it is now dealing with a reference type, using the same specialization trick

template<typename T, bool isReference>
class A {
};

template<typename T>
class A<T,false>{
private:
T m_member;

public:
A(T _member);
//... MORE STUFF
void foo(T param);
}
/////////////////////////
template<typename T>
class A<T,true>{
private:
T& m_member;

public:
A(T& _member);
//... MORE STUFF
void foo(T param);
}

If you want to extract some similar behavior and avoid the code dupelication this solution causes, you could easily extract that behavior to a Base Class<T>, and do

template<typename T,bool isReference>
class A : public BaseClass<T>{
}

and so on.

Usage would be

main.cpp

A<int,false> a1;//1st version
A<int&,true> a2;//2nd version
A<B,false> a3;//1st version
A<B&,true> a4;//2nd version
A<C*,false> a5;//1st version, as pointers are value types

Template specialization and references

only the general (or primary? What is the correcto word here?) template

The technical term used by the C++ Standard is "primary class template". It will also be the most general class template, compared to its partial specializations and explicit specializations. So that could also be a reasonable thing to call it, given enough context.

The "reference collapsing rule" is found in [dcl.ref]/6 and applies mainly when determining the meaning of combining a specific type name which aliases a reference type with a & or && token which would normally form a reference to the type name's type. Deducing template arguments for a template parameter of the form T& or T&& is sort of the reverse of that. Although it's helpful to think of template argument deduction as "find the template arguments so that the resulting types match up", the technical details of template argument deduction are much more specific; [temp.deduct] is several pages of rules for exactly how this deduction proceeds, and there are additional relevant rules in other sections. The detail is needed so compilers agree on cases when there could otherwise be more than one "correct" answer, and so that compilers aren't required to deal with some of the more difficult cases.

In particular, when matching a dependent type P with a known type A, by the list of deducible types in [temp.deduct.type]/8, deduction can occur if both P and A have the form T& or if both have the form T&&. When attempting argument deduction for the partial specialization remove_reference<T&&> to determine the definition of remove_reference<int&>, P is T&& and A is int&, so they do not share one of these forms.

The template argument deduction rules do not have a general allowance for deducing arguments from a reverse of the reference collapsing rule. But they do have a limited allowance which is related for certain cases: Per [temp.deduct.call]/3, if T is a template type parameter, but not a parameter for a class template, then the type T&& is a forwarding reference. When comparing types for argument deduction, if P=T&& is a forwarding reference type and A is an lvalue reference type, then the template type parameter T can be deduced as the lvalue reference type A, only if A is the type of an lvalue function argument expression ([temp.deduct.call]/3 again) or sometimes if P and A are being compared because they represent function parameter types within two compared function types ([temp.deduct.type]/10).

Similarly, when ["]calling["] remove_reference_t<int&&>, can't the first specialization's T& match int&& if T is substituted for T&?

In this case, there's no possible way that the partial specialization remove_reference<T&> can match remove_reference<int&&>. Even if the process of template argument deduction allowed finding a potential answer for this case, there is no possible type T such that T& is the same as int&&.

Allow Only Explicit Specialization of Template Class

There are a few ways how you could accomplish this.

1. Explicitly list the acceptable enums

One way would be to explicitly list the acceptable enums in your static_assert:

godbolt

#include <type_traits>

template<class T, class... Other>
constexpr bool is_same_one_of = (std::is_same_v<T, Other> || ...);

enum Enum1 {};
enum Enum2 {};
enum Enum3 {};

template<class T>
class kvEnumHelper {
static_assert(
is_same_one_of<T, Enum1, Enum2 /* , ... more enum types ... */>,
"T must be either Enum1 or Enum2"
);

/* ... actual implementation ... */
};

kvEnumHelper<Enum1> foo1; // ok
kvEnumHelper<Enum2> foo2; // ok
kvEnumHelper<Enum3> foo3; // compile error


2. Inherit implementation

Another option would be to move the actual implementation into a separate class and only make the specializations inherit from the implementation class.

godbolt

#include <type_traits> 

enum Enum1 {};
enum Enum2 {};
enum Enum3 {};

template<class T>
class kvEnumHelper {
static_assert(
!std::is_same_v<T, T>, // always false, but only when actually instanciated
"Enum Class is not supported"
);
};

template<class TEnum>
class kvEnumHelperImpl {
/* ... actual implementation ... */
};

template<> class kvEnumHelper<Enum1> : public kvEnumHelperImpl<Enum1> {};
template<> class kvEnumHelper<Enum2> : public kvEnumHelperImpl<Enum2> {};

kvEnumHelper<Enum1> foo1; // ok
kvEnumHelper<Enum2> foo2; // ok
kvEnumHelper<Enum3> foo3; // compile error


3. Using an additional trait

Yet another alternative would be to use a trait that can specialized for the enum types you would want to be usable with kvEnumHelper.

godbolt

template <class T>
constexpr bool allow_enum_helper = false;

enum Enum1 {};
enum Enum2 {};
enum Enum3 {};

template<class T>
class kvEnumHelper {
static_assert(
allow_enum_helper<T>,
"Enum Class is not supported"
);

/* ... actual implementation ... */
};

template<>
constexpr bool allow_enum_helper<Enum1> = true;
template<>
constexpr bool allow_enum_helper<Enum2> = true;

kvEnumHelper<Enum1> foo1; // ok
kvEnumHelper<Enum2> foo2; // ok
kvEnumHelper<Enum3> foo3; // compile error

If you already have a function like getEnumFromString that is deleted and has specializations for the allowable enum types you could use that to detect if kvEnumHelper<T> should be allowed by detecting if the function is deleted or not.

godbolt

#include <string>

enum Enum1 {};
enum Enum2 {};
enum Enum3 {};

template<typename T>
T getEnumFromString(const std::string& in_string) = delete; // only allow templates we define (catches them at compile time)
template<> Enum1 getEnumFromString(const std::string& in_string);
template<> Enum2 getEnumFromString(const std::string& in_string);

template<class T>
constexpr bool allow_enum_helper = requires { getEnumFromString<T>(std::string{}); };

template<class T>
class kvEnumHelper {
static_assert(
allow_enum_helper<T>,
"Enum Class is not supported"
);

/* ... actual implementation ... */
};

kvEnumHelper<Enum1> foo1; // ok
kvEnumHelper<Enum2> foo2; // ok
kvEnumHelper<Enum3> foo3; // compile error

Function with single optional parameter and default value in template function

Mmm the other answer is close. But not quite there. f(12) doesn't "try to instantiate f<int&&>". In fact, it fails to deduce T because T is in non-deduced context.

Also, your question was beside the point: even without a default value you have the same problem: Compiler Explorer

template <typename T> void f(boost::optional<T> v) {
if (v) {
std::cout<<"value: " << v.get() << "\n";
} else {
std::cout<<"no value\n";
}
}

int main()
{
f(12);
f("string");
}

Now, before I blindly show you how you can fix all that, ask yourself the question: What are we doing here.

If you want default arguments, doesn't that by definition mean that they aren't optional values? Maybe you simply need: Compiler Explorer

template <typename T> void f(T const& v) {
std::cout << "value: " << v << "\n";
}
void f() {
std::cout << "no value\n";
}

int main()
{
f(12);
f("string");
f();
}

Printing

value: 12
value: string
no value

With some hackery you can combine the overloads by defaulting the template type argument:

template <typename T = struct not_given*> void f(T const& v = {}) {
if constexpr(std::is_same_v<T, not_given*>) {
std::cout << "no argument\n";
} else {
std::cout << "value: " << v << "\n";
}
}

Prints Compiler Explorer

value: 12
value: string
no argument

What If You Require optional<>

In that case, in you specific example you would probably want optional<T const&> to avoid needlessly copying all the arguments; but see std::optional specialization for reference types.

If You Really Really Want¹

Say, you MUST have the semantics you were looking for. You do not care that you won't be able to know the difference between calling with no argument vs. calling with an uninitialized optional (none). This is kinda like many scripting languages, right?

Now you have to make the template argument become deduced context, and then want to ensure that... it is an optional<T>:

template <typename T, typename = void> struct is_optional : std::false_type { };
template <typename T> struct is_optional<boost::optional<T>> : std::true_type { };

template <typename T = boost::optional<void*> >
std::enable_if_t<is_optional<T>::value> f(T const& v = {}) {
if (v) {
std::cout << "value: " << *v << "\n";
} else {
std::cout << "no value\n";
}
}

template <typename T>
std::enable_if_t<not is_optional<T>::value> f(T const& v) {
return f(boost::make_optional(v));
}

int main()
{
f(12);
f("string");
f();
}

One "advantage" is that that now you clearly see the copying being done.

Another "advantage" is that now you can support std::optional the same way: https://godbolt.org/z/1Mhja83Wo

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

Summary

I hope this answer gets the point across that C++ is not a dynamically typed language. This implies that the idea of optional arguments of "unknown" type is really not idiomatic. (It might be a bit unfortunate that Boost called it boost::none instead of e.g. std::nullopt, perhaps giving people associations with Python's None.)

Instead, you can use static polymorphism. The simplest version of that was the first I showed, using function overloading.

If you were to mimic a dynamic type interface in C++, you would probably use std::variant or std::any instead. To restrict the bound types you would use concepts (this is getting a bit deep, but see e.g. Boost Type Erasure).


¹ i really really really wanna zig a zig ah



Related Topics



Leave a reply



Submit