C++: Unexpected Behaviour

Antonio Mallia

Jaime Alonso

About Us

Antonio Mallia

Jaime Alonso

Motivation

Definitions

  • The behaviour of well-formed C++ programs not defined by the standard can fall under the following categories:
    • Implementation-defined
    • Unspecified
    • Undefined
  • Any of these can result in unexpected behaviour!

Let's abbreviate Unexpected Behaviour as UXB

UXB occurs when a program does not perform what the programmer initially intended and instead causes an undesired result.

Arrays


                        int a[10];
                        ...
                        a[5] = 1;

                        5[a] = 1;
                        
The C standard defines the [] operator as follows:

                        a[5] == *(a + 5)
                        
Therefore 5[a] will evaluate to:
*(5 + a) == *(a + 5)

Encapsulation

“ Encapsulation is used to hide the values or state of a structured data object inside a class, preventing unauthorized parties' direct access to them.”
Wikipedia

                        class Account {
                        public:
                            virtual void deposit(int amount) {
                                accountBalance += amount;
                            }
                            virtual void balance() {
                                std::cout << "Balance: " << accountBalance << std::endl;
                            }
                        protected:
                            int accountBalance = 0;
                        };
                        
                        class EmployeeAccount : public Account {
                        private:
                            virtual void paySalary() { accountBalance += 1000; }
                        };
                        

                        int main()
                            Account a;
                            a.deposit(100);
                            a.balance(); // 100

                            EmployeeAccount e;
                            e.paySalary(); // won't compile
                        }
                        

                        class Account {
                        public:
                            virtual void deposit(int amount) {
                                accountBalance += amount;
                            }
                            virtual void balance() {
                                std::cout << "Balance: " << accountBalance << std::endl;
                            }
                            virtual void paySalary() {}
                        protected:
                            int accountBalance = 0;
                        };
                        class EmployeeAccount : public Account {
                        private:
                            virtual void paySalary() { accountBalance += 1000; }
                        };
                        

                        int main()
                            Account a;
                            a.deposit(100);
                            a.balance(); // 100

                            EmployeeAccount e;
                            
                            e.paySalary(); // won't compile
                            
                            
                            Account* ea = &e;
                            ea->paySalary();
                            a.balance(); // 1000
                        }
                        

                            while(true){
                                ea->paySalary();
                            }
                        

The long arrow operator


                            size_t x = 10;
                            while(0<--x);
                            std::cout << x << std::endl;  // 0 
                        

<-- is not an operator

It is equivalent to:

                            while(0 < (--x));
                            

What if we make it longer?


                            wp----->size();
                        

The long arrow is not a single operator, but a combination of multiple operators.

Here it is a normal -> operator and the postfix decrement operator --

                            ((wp--)--)->length();
                        

How can we overload them?

Implicit conversions

It can be a double-edged sword

                        void greeting(std::string str) {
                            std::cout << str << std::endl;
                        }
                        
                        void greeting(bool german) {
                            if(german)
                                std::cout << "Hallo Welt!" << std::endl;
                            else
                                std::cout << "Hello World!" << std::endl;
                        }
                        
                        
                        greeting("Ciao mondo!"); // Ciao mondo!
                        
                        
                        greeting("Ciao mondo!"); // Hallo Welt!
                        
                        
  • Certain types of conversions take precedence
  • Standard conversions take precedence over user-defined conversions
  • `const char*` to `bool` over `const char*` to `std::string`
  • Variable `a`'s underlying type is bool!
  • We can prevent this from happening by using C++14's string literals

                        using namespace std::string_literals;
                        std::variant<int, bool, std::string> a = "Hello World!"s;

                        // Now we get the expected std::string type
                        

Overload resolution


                        #include <vector>
                        #include <algorithm>

                        bool isOne(int a) { return a == 1; }
                        bool isOne(double b) { return a == 1.0; }

                        int main()
                        {
                            std::vector<int> my_vec{1,2,3,4};
                            // doesn't compile
                            std::find_if(my_vec.begin(), my_vec.end(), isOne);
                        }
                        

* https://www.fluentcpp.com/2017/08/01/overloaded-functions-stl/

Type deduction can help us

                        struct wrapper
                        {
                            template<typename T>
                            bool operator()(T&& a)
                            {
                                return isOne(a);
                            }
                        };

                        std::find_if(my_vec.begin(), my_vec.end(), wrapper());
                        
                        std::find_if(my_vec.begin(), my_vec.end(),
                                     [](auto&& a){ return isOne(a); });
                        
                        

Overload resolution II

Ternary operator

Optimizations and UXB

Optimizations: code elimination

Optimizations: simplifications

Optimizations: static initialization

  • Optimizations can also introduce other unexpected problems
    • Optimizing away data reads of variables (use `volatile`)
    • Not zero-initializing uninitialized integers
  • Many of these can be found with compiler warnings

Copy elision

Conclusion

  • Unexpected behaviour can be:
    • Compiler optimizations
    • Obscure standard specifications
  • How do we prevent this?
    • Enable compiler warnings and address them (`-Wall -Wextra`)
    • Use static analysis regularly (as part of CI)
  • Sometimes it's not enough!

Where to find these slides

http://cpp-unexpected-behaviour.github.io/meetingcpp2017

Thank you!