Trying to understand: clang's side-effect warnings for typeid on a polymorphic object

320 Views Asked by At

This question is not about how to avoid the described warning. (Store in a reference beforehand; Or use dynamic_cast instead of typeid)

I'm trying to understand why the warning exists in the first place. What is it trying to protect from?

Consider the following code example:

#include <memory>
#include <iostream>

struct A { virtual ~A() = default; };

struct B : A { };

A *get_pa() { 
    static A static_a{};
    return &static_a; 
}

int main() {
    std::shared_ptr<A> pa1 = std::make_shared<B>();
    A *pa2 = new B{};

    //1) warning: will be evaluated
    std::cout << typeid(*pa1).name() << '\n';         

    //2) okay
    std::cout << typeid(pa1.get()).name() << '\n';  
    
    //3) okay    
    std::cout << typeid(*pa2).name() << '\n';               

    //4) warning: will be evaluated
    std::cout << typeid(*new A{}).name() << '\n';
    
    //5) warning: will not be evaluated
    std::cout << typeid(new A{}).name() << '\n'; 

    //6) warning: will be evaluated
    std::cout << typeid(*get_pa()).name() << '\n';  

    //EDIT 1:
    //7) okay
    std::cout << typeid(get_pa()).name() << '\n';
}

Compile this code with clang and you'll get the following kind of warning:

warning: expression with side effects will be evaluated despite being used as an operand to 'typeid' [-Wpotentially-evaluated-expression]
    std::cout << typeid(*pa1).name() << '\n';
                        ^

I think I understand the general premise of that warning:

  • typeid will actually evaluate the expression these cases, and we might not be aware of that.

  • The expression involves a polymorphic reference. Even if its class doesn't cause any side-effects, classes that inherit from it might have side-effects.

  • The expression *pa1, is equivalent to *pa1.get() which calls a function returning a pointer. We might not notice/consider the fact that the dereferencing is overloaded (We might've thought it was a raw pointer, or maybe the code originally used a raw pointer and was later changed to use a smart pointer)

All of the warned expressions take a pointer to A* and dereference it (except for example 5). How can dereferencing a pointer cause a side-effect? And why don't we get a warning for the expressions that don't dereference?

EDIT 2: Could you give an example of a class inheriting from A that would cause a side effect where it gives a warning it might happen, but not cause a side effect where it doesn't give such a warning? EDIT: Considering the answers & comments, the question in EDIT 2 is very likely a red herring, and the answer most likely involves value category rules and typeid rules, rather than directly involving polymorphism and its potential for overrides.

EDIT 3: Observations:

  1. As examples 6 & 7 show. The compiler doesn't really care about the expression returning a pointer to a polymorphic object having a potential side effect, but rather, it cares about the dereferencing of it having a side effect.

  2. Whether we dereference a pointer or a smart pointer doesn't seem to matter, so it doesn't seem to be about overriding the dereferencing operation.

  3. If we make A non-polymorphic (remove the virtual destructor definition) it would not show the warning for evaluating with a potential side effect (it still shows the opposite warning for example 4,5) further emphasizing that the problem is not with an expression returning a pointer but with the dereferencing them.

  4. and yet, example 3 seems to contradict the other observations, since if the problem was with dereferencing it, it shouldn't matter whether the pointer itself has a name or not.

  5. Thus far any explanation of what we are being warned for, as well as when the compiler looks ahead to consider a warning and when it doesn't seems to be inconsistent.

1

There are 1 best solutions below

5
On BEST ANSWER

You already explain the purpose of the warning correctly, so I will just go through the list to explain why it does or does not apply in each case:

//1) warning: will be evaluated
std::cout << typeid(*pa1).name() << '\n';

pa1 is not a pointer, it is a class type, a std::shared_ptr<A>. It is true that the dereferencing still doesn't have any side effect, but to determine that for sure the compiler would need to figure out what the overloaded operator* exactly does. Compiler's don't usually do such deeper analysis for warnings. That it still warns although it can't prove whether side effects are possible either way is a choice made by the compiler developers.

//2) okay
std::cout << typeid(pa1.get()).name() << '\n';

This one is okay because pa1.get() is a pointer type. It is not a polymorphic glvalue and therefore the expression will not be evaluated at all. typeid only evaluates the operand if it is a polymorphic glvalue, which is part of why one can easily get confused whether the operand will cause side effects. typeid(pa1.get()) is always A*, determined at compile-time.

//3) okay    
std::cout << typeid(*pa2).name() << '\n';     

This one is okay because you are really only dereferencing a pointer which, without having to look through other functions, is guaranteed not to have side effects.

//4) warning: will be evaluated
std::cout << typeid(*new A{}).name() << '\n';

This has a side effect because it allocates memory. The warning here is really useful, because *new A{} is going to lead to a guaranteed memory leak, so the side effect is definitively unintended.

//5) warning: will not be evaluated
std::cout << typeid(new A{}).name() << '\n'; 

As above new A{} evaluates to a pointer prvalue, not a polymorphic glvalue, so the expression won't be evaluated. But new A{} has a guaranteed side effect, so why would you have written it if you don't intent the side effect? Just A* would be fine and less prone to mistake.

//6) warning: will be evaluated
std::cout << typeid(*get_pa()).name() << '\n';

Again, the compiler is not likely to look through the get_pa call to determine whether it has side effects. In this case get_pa does have a side effect. It initializes the static variable. So in any case the warning is warranted.


In any case, the warnings can be avoided and the intention be made clearer by simply not passing anything but an id-expression to typeid. For example you can state clearly that evaluation will happen like this:

auto& obj = *get_pa();
std::cout << typeid(obj).name() << '\n';