5

I am learning about templates in C++, and came across an example where casting to void is used:

template<typename T>
auto func (T const& t) -> decltype( (void)(t.size()), T::size_type() )
{
return t.size();

}

In the explanation it is written that:

The cast of the expression to void is to avoid the possibility of a user-defined comma operator overloaded for the type of the expressions.

My question(s) is/are:

  1. How can a cast to void be used to "avoid the possibility of a user-defined comma operator overloaded for the type of the expressions"? I mean, can anyone give any example where if we don't use void then this code would give an error? For example, let's say we have a class called SomeClass which has overloaded the comma operator. Now, can this become a problem if we don't use void?

  2. Can static_cast be used in this case instead of a C style cast? For example, something like static_cast<void>(t.size()). I am reading examples that use C++17 features, and so I wonder why the author has used a C style cast in this case.

I have read What does casting to `void` really do?, from which I get the impression that if we use (void)x then this means to suppress compiler warnings, and also means "ignore the value of x". But then I can't understand the difference between the expression x and (void)x.

Remy Lebeau
  • 555,201
  • 31
  • 458
  • 770
Jason
  • 36,170
  • 5
  • 26
  • 60
  • There's nothing stopping `T::size()` from returning an object which might have overloaded the comma operator. In which case `t.size(), T::size_type()` would be the same as `t.size().operator,(T::size_type())` – Some programmer dude Sep 24 '21 at 11:58
  • 1
    *For example lets say we have a class called SomeClass which has overloaded the comma operator. Now can this become a problem if we don't use void.* That's the example. For class types, we don't know if `,` is regular or doing bitwise inexclusive or ;) – NathanOliver Sep 24 '21 at 11:58

1 Answers1

7

Consider this pathological type:

struct foo {
    struct size_type {
        bool operator,(size_type) { return false;}
    };
    size_type size() { return {};}  
};

It does have a size_type and it does have a size() method. However, without the cast to void, the template does not deduce the right return type, because decltype( (t.size()), typename T::size_type() ) is bool :

#include <type_traits>

template<typename T>
auto func (T const& t) -> decltype( (t.size()), typename T::size_type() )
{
return t.size();

}

struct foo {
    struct size_type {
        bool operator,(size_type) { return false;}
    };
    size_type size() const { return {};}  
};


int main()
{
   func(foo{});
}

Results in error:

<source>:6:8: error: no viable conversion from returned value of type 'foo::size_type' to function return type 'decltype((t.size()) , typename foo::size_type())' (aka 'bool')
return t.size();
       ^~~~~~~~
<source>:20:4: note: in instantiation of function template specialization 'func<foo>' requested here
   func(foo{});
   ^
1 error generated.
ASM generation compiler returned: 1
<source>:6:8: error: no viable conversion from returned value of type 'foo::size_type' to function return type 'decltype((t.size()) , typename foo::size_type())' (aka 'bool')
return t.size();
       ^~~~~~~~
<source>:20:4: note: in instantiation of function template specialization 'func<foo>' requested here
   func(foo{});
   ^

A static_cast can be used. However, as nothing is actually being cast (it is an unevaluated context), the c-style cast does not do much harm. Note how by using the cast to void the user defined operator, is bypassed and the correct return type is deduced: https://godbolt.org/z/jozx1YGWr. This is because void, whatever uses the built-in operator, whose result is of same type as whatever. You cannot override void, whatever with a user-defined operator,; there is no syntax that works.

I suppose the code is merely to illustrate this one effect, because even with the cast to void one can make up other examples that fail (eg the template does not explicitly test for t.size() actually returning T::size_type).


See also the section on "Rarely overloaded operators" here:

The comma operator, operator,. Unlike the built-in version, the overloads do not sequence their left operand before the right one. (until C++17) Because this operator may be overloaded, generic libraries use expressions such as a,void(),b instead of a,b to sequence execution of expressions of user-defined types. The boost library uses operator, in boost.assign, boost.spirit, and other libraries. The database access library SOCI also overloads operator,.

463035818_is_not_an_ai
  • 109,796
  • 11
  • 89
  • 185
  • I don't get how adding(that is casting to) `void` would remove this error. The result of the evaluation of the first expression in comma operator is discarded anyway. Though since `decltype` is used the expression inside it will not be evaluated. How does adding `void` fix the problem. That is if we have the expression `E1, E2` then `E1` is evaluated and the result is discarded. So even if we cast `E1` to `void` its result should still be discarded. How does this help solve the overloading issue. – Jason Sep 24 '21 at 12:17
  • @JasomLiam :) I was already afraid that detail was missing. There is no custom `operator,` for void. It uses the built in one (that you already know ;). See edit – 463035818_is_not_an_ai Sep 24 '21 at 12:20
  • 1
    @JasonLiam It is impossible to declare a user-defined `operator,` that has a parameter of type `void`. Therefore, the only `operator,` that can be used when one of the arguments has type `void` is the built-in operator. – j6t Sep 24 '21 at 12:37