You can try to use
find -D tree . [expr..] to understand what find does with your original command.
You must understand that the
-type f and also the
-exec ls .. expressions are
and'ed to the rest of the expressions with higher precedence than the
So your original command will get parsed into something like this:
(-type f AND -name *.c) OR -name *.h OR (-name *.cpp AND -exec ls) (note that the or is actually binary and not ternary so
a | b | c is in fact
(a | b) | c, but you get the point).
Now you will notice that find does not know what to do except for files matching
*.cpp as there is no valid statement in the other cases (that's why you may even see a segfault or something similar in the debug output).
I hope this makes it more clear to you why you need the parentheses.