Casting to void to avoid use of overloaded user-defined Comma operator
Asked Answered
N

1

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.

Nan answered 24/9, 2021 at 11:52 Comment(2)
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())Petiolule
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 ;)Ewaewald
C
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,.

Crifasi answered 24/9, 2021 at 12:1 Comment(3)
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.Nan
@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 editCrifasi
@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.Jasen

© 2022 - 2024 — McMap. All rights reserved.