The art
of keeping your project
from falling apart


Martin Šmarda
martin.smarda@kozodoj.cz

About me

  • C++ developer in Exasol
  • NGO manager and volunteer
  • horse trainer and coach


WARNING:
this presentation contains personal opinions

Who do we write the code for?

For compilers to parse and optimize,

for the processor to execute,

or for programmers to read and understand?

What can we say about programmers

They are human beings.
They have motivations, goals, feelings, ego.
They are lazy and rely on habits.
They don't like being wrong.
They are not good at handing complexity.

Spoiler alert: this is the core slide

How good are humans with handling complexity?

Limits of perception

What is the depth of human mental stack, before overflow?

How many registers human have?

Limits of perception

Limits of perception

Limits of perception

Limits of perception

Limits of perception

Limits of perception

Limits of perception

Limits of perception

How many rectangles was this image created from?

How many rectangles was this image created from?

How many rectangles was this image created from?

Did you realized the change in background color?

How good are humans with handling complexity?

  • can hold cca 5 things in memory
  • can focus on one complex task only and ignore the rest


Compensations

  • learned knowledge
  • habbits
  • expectations

Why should this matter to a programmer?


								draw_rectangle(42, -11, 13, 17, 209, 162, 30, true, true);
							

								void draw_rectangle(int x, int y, int width, int height, 
									uint8_t colorR, uint8_t colorG, uint8_t colorB, 
									bool isVisible = true, bool drawBorder = false);
							

DRY
Don't Repeat Yourself

Can this rule of thumb be counterproductive?

Repetition reduction increases complexity
and makes code harder to modify.


								void cat()
								{
									std::clog << "start cat\n";
								
									look_at_the_door();
									eat("mouse");
									sleep();
								}
								
								void dog()
								{
									std::clog << "start dog\n";
								
									bark();
									eat("your dinner");
									sleep();
								}
						

								void cat()
								{
									std::clog << "start cat\n";
								
									look_at_the_door();
									eat("mouse");
									sleep();
								}
								
								void dog()
								{
									std::clog << "start dog\n";
								
									bark();
									eat("your dinner");
									sleep();
								}

								void pet(const std::string& species, const std::string& food, 
								         const std::function<void(void)>& action)
								{
									std::clog << "start " << species << '\n';

									action();
									eat(food);
									sleep();
								}
						

								void pet(const std::string& species, const std::string& food, 
								         const std::function<void(void)>& action)
								{
									std::clog << "start " << species << '\n';

									action();
									eat(food);
									sleep();
								}

								void cat()
								{
									pet("cat", "mouse", [](){ look_at_the_door(); } );
								}

								void dog()
								{
									pet("dog", "your dinner", [](){ bark(); } );
								}
						

								void pet(const std::string& species, const std::string& food, 
								         const std::function<void(void)>& action)
								{
									std::clog << "start " << species << '\n';

									action();
									eat(food);
									sleep();
								}

								void puppy()
								{
									std::clog << "start puppy\n";
	
									eat("your shoe");
									eat("your dinner");
									tear_apart_your_home();
									sleep();
								}
						

							void pet(const std::string& species, const std::string& food, 
										const std::function<void(void)>& action)
							{
								std::clog << "start " << species << '\n';

								action();
								eat(food);
								sleep();
							}

							void cat()
							{
								pet("cat", "mouse", [](){ look_at_the_door(); } );
							}

							void dog()
							{
								pet("dog", "your dinner", [](){ bark(); } );
							}
						

							void pet(const std::string& species, const std::function<void(void)>& action)
							{
								std::clog << "start " << species << '\n';
							
								action();
								sleep();
							}
							
							void cat()
							{
								pet("cat",
									[]()
									{
										eat("mouse");
										look_at_the_door();  
									} 
								);
							}
							
							void puppy()
							{
								pet("puppy",
									[]()
									{
										eat("your shoe");
										eat("your dinner");
										tear_apart_your_home(); 
									} 
								);
							}
						

DRY
Don't Repeat Yourself

If your code fits the abstraction, you don't have to make updates in multiple places.

... at the cost of increased complexity.

DRY
Don't Repeat Yourself

    Recommendations

  • DRY your code once it is finished.
  • Think about interface you use for abstraction.
  • Be hesitant to DRY high-level concepts from unrelated code.


Humans are not good at handling complexity.

Write your code for the code review

Where do you do your code reviews?

Implications

Browser is not an IDE.

Almost always auto

Code is easier to write but can be harder to read.

auto.cpp:18:9: error: cannot bind non-const lvalue reference 
                      of type 'uint8_t&' {aka 'unsigned char&'} 
                      to an rvalue of type 'uint8_t' {aka 'unsigned char'}
18 |     bar(a);
   |         ^
							

							// generated by https://cppinsights.io/
						
							#include <cstdint>

							extern void foo(const uint8_t &);
							extern void bar(uint8_t &);
										
							int main()
							{
								class __lambda_8_22 { /* ... */ };
								
								const __lambda_8_22 add = __lambda_8_22{};
								const uint8_t x = 13;
								const uint8_t y = 42;
								int a = add.operator()(x, y);
								foo(static_cast<const unsigned char>(a));
								// !?? bar(a);
							}
						

Write your code for the code review

  • Think how you navigate your reader's mind.
  • Do not overuse auto.
  • Keep your pull/merge requests small.

What are the code reviews for?

What can we say about programmers

They are human beings.
They have motivations, goals, feelings, ego.
They are lazy and rely on habits.
They don't like being wrong.
They are not good at handing complexity.

Spoiler alert: this is the core slide

							class LegacyClass
							{
								uint8_t* owningPtr;
							
							public:
								LegacyClass(size_t size)
								{
									owningPtr = new uint8_t[size];
								}
							
								~LegacyClass()
								{
									delete owningPtr;
								}
							
								void legacy_method() 
								{
									// may throw an exception, make sure memory is deallocated
									std::unique_ptr<uint8_t> uptr(owningPtr);
									owningPtr = nullptr;
							
									// ...
									throw std::runtime_error("");
								}
							};
						

Which behavior of your programmers do you incentify?

Don't practice what you do not want to become.
Jordan Peterson

It is hard to change company culture. Be carefull what culture you are creating.

Pick the right bully

Automated bullies tools

continuous integration
sanitizers - ASAN, UBSAN, TSAN
static analyzers - clang tidy
fuzzers
runtime debug checks
tests
staged release, telemetry

Code review

Is not about nitpicking.
Is not about finding design flaws.
Review is about understanding the program.
Review is about understanding the programer.

Workflow incentives

What is the cost of running a test?
How many merge reqest can be merged in a day?

Documentation

Unless you have a dedicated department for it...
it can be outdated, non existent or written in the code

Documentation

Documentation should contain intentions and reasoning behind the code.


						// The only practically maintainable form of documentation
						// is documentation written in the code.
						int main()
						{
							// What prevents you from writing few sentences, even a paragraph, 
							// directly at the place where it matters the most?

							// BWT, documentation should help the reviewer to understand your code
							// before merging.
							return 0;
						}
						

Issue tracking

... is part of the documentation.


								// ISSUE-123 Fixing missing example of practical issure tracking 
								//           in my presentation
								int main()
								{
									return 0;
								}
							

								# git commit -m "Commit demonstrating issue tracking (ISSUE-123)"
							

Constants

... are part of the documentation.

Name your constants. Even if you use them once.


								void foo(std::vector<int> vec)
								{
									if(vec.size() > 13 || vec.size() < 7)
									{
										// ...
									}
								}
							

								void foo(std::vector<int> vec)
								{
									const auto upperSizeThreshold = 13; // reasoning why it is 13
									const auto lowerSizeThreshold = 7; // reasoning why it is 7
									if(vec.size() > lowerSizeThreshold || vec.size() < upperSizeThreshold)
									{
										// ...
									}    
								}
							

Constant expressions


							enum bitmask1
							{
								flag_a = 1,
								flag_b = 2,
								flag_c = 4,
							};
						
						
								enum bitmask2
								{
									flag_a = 1 << 0, // what flag a means
									flag_b = 1 << 1, // what flag b means
									flag_c = 1 << 2, // what flag c means
								};
							

String constants

Beware of traps in string literals.


							const char binaryLiteral0[] = "\x12\x34";
						

							const char binaryLiteral1[] = "\x00\x01ALOHA";
							sizeof(binaryLiteral1) == /* ? */; // 7
						

							const char binaryLiteral2[] = "\x00\x00000000000000001ALOHA";
							sizeof(binaryLiteral2) == 7;
						

							const char binaryLiteral3[] = "\x00\x01" "ALOHA";
							sizeof(binaryLiteral3) == 8;
						

Constrains

... are part of the documentation.

						
							void process_answer(const int number)
							{
								assert(number == 42);
								// ...
							}

							void process_answer(const int number)
							{
								assert(number == 42 && "Universal answer must be 42");
								// ...
							}
						

Constrains

Use compile time checks as much as you can for hidden expectations.

						
							//// in some include
							// using typeA_t = ???;
							// using typeB_t = ???;
							
							void foo(typeA_t a, typeB_t b);	
						
						
							void foo(const typeA_t a, const typeB_t b)
							{
								static_assert(std::is_same_v<typeA_t, typeB_t>, 
									"The types does not match, check the code below.");
						
								static_assert(std::is_integral_v<typeA_t>, 
									"The typeA_t is not integral, check the code below.");

								// ...
							}
						

Error handling

return codes


							bool getData(const std::string& url, std::vector<uint8_t>& outData);
							/* DWORD getData(const std::string& url, std::vector<uint8_t>& outData); */
							
							std::vector<uint8_t> data;
							const bool success = getData("meetingcpp.com", data);
							if(success)
							{
								//...
							}
						

Error handling

exceptions

			
							std::vector<uint8_t> getData(const std::string& url);

							std::vector<uint8_t> data;
							try
							{
								data = getData("meetingcpp.com");
							}
							catch(const std::runtime_error& ex)
							{
								// ...
							}
						

Error handling

std::optional


							std::optional<std::vector<uint8_t>> getData(const std::string& url);

							const std::optional<std::vector<uint8_t>> data = getData("meetingcpp.com");
							if(data)
							{
								// ...
							}
						

Error handling

structured bindings


							std::tuple<bool, std::vector<uint8_t>> getData(const std::string& url);

							auto [success, data] = getData("meetingcpp.com");
							if(success)
							{
								// ...
							}
						

Making the code hard to misuse

Interface elevation

Inspired by sudo (Linux) and UAC (Windows)

Interface elevation

						
							class BasicInterface
							{
							public:
								virtual ~BasicInterface() {}
							
								virtual int getValue() const = 0;
								virtual void setValue(int) = 0;
							};
						

							class ElevatedInterface : public BasicInterface
							{
							public:
								virtual ~ElevatedInterface() {}
							
								virtual std::lock_guard<std::mutex> sync() = 0;
								virtual void addValueListener(std::function<void(int)>) = 0;
							};
						

							class Implementation : public ElevatedInterface
							{
								// ...    
							};
						

Interface elevation

						
							extern void engineFunction(ElevatedInterface&);
							extern void userFunction(BasicInterface&);
							
							int main()
							{
								Implementation instance;
							
								engineFunction(instance);
								userFunction(instance);
							
								return 0;
							}
						

								void userFunction(BasicInterface& iface)
								{
									// If user code needs for some reason elevated interface,
									// it has to be casted.
									ElevatedInterface& elevatedIface = dynamic_cast<ElevatedInterface&>(iface);
								}
							

Making the code hard to misuse
... at a cost

Autoregistration

... like in Catch unit-testing framework


							SCENARIO( "vectors can be sized and resized", "[vector]" ) 
							{
								GIVEN( "A vector with some items" ) 
								{
									std::vector v( 5 );	
									REQUIRE( v.size() == 5 );
								}
							}
						

How to implement it in your own framework


							class Interface
							{
							public:
								virtual ~Interface() {}
							
								virtual void foo() const = 0;
							};
							

							using factoryFunc_t = std::unique_ptr<Interface>(void);

							std::vector<std::function<factoryFunc_t>>& getAutoregistrationContained()
							{
								static std::vector<std::function<std::unique_ptr<Interface>(void)>> container;
								return container;
							}
						

							template<typename DERIVED>
							class Parrent : public Interface
							{
							public:
								virtual ~Parrent() {}
							
							protected:
							
								static std::unique_ptr<Interface> createInstance( )
								{
									return std::unique_ptr<Interface>( new DERIVED() );
								}
							
								class RegistrationHelper
								{
								public:
									RegistrationHelper() : registered(true)
									{
										auto& container = getAutoregistrationContained();
										container.push_back(createInstance);
									}
								};
							
								static RegistrationHelper s_registrationHelper;
							};
						

							class Child : public Parrent<Child>
							{
							public:
								virtual ~Child() {}

								void foo() const override
								{
									std::cout << "foo" << '\n';
								}
							};

							template<typename DERIVED>
							typename Parrent<DERIVED>::RegistrationHelper Parrent<DERIVED>::s_registrationHelper;
							template class Parrent<Child>;
						

							int main()
							{
								std::cout << "size " << getAutoregistrationContained().size() 
											<< " autoregistered classes\n";
							
								for(std::function<factoryFunc_t>& factory : getAutoregistrationContained())
								{
									std::unique_ptr<Interface> instance = factory();
									instance->foo();
								}
							
								return 0;
							}							
						

Making the code hard to misuse
... at a cost

Is it worth it?

Making the code hard to misuse
... at a too high cost

STL breaks encapsulation

by forcing you to expose the container type with its iterators.


									class Foo;

									class Implementation : public Interface
									{
									public:
									// ...
									
									private:
										std::vector<Foo> container;
										// std::list<Foo> container;
									};
							

								class Interface
								{
								public:
									virtual std::vector<Foo>::iterator begin() = 0;
									virtual std::vector<Foo>::iterator end() = 0;
								};
							

							class Interface
							{
							public:
								using callback_t = void(Foo&);
								virtual void for_each(const std::function<callback_t>&) = 0;
							};
						

								Implementation instance;
								Interface2& interfacedInstance = instance;

								instance.for_each
								(
									[](Foo& foo)
									{
										// do the single iteration
									}
								);
							

Or create your own iterator hiding the real one...

what leads you into a deep, deep rabbit hole.

What can we say about programmers

They are human beings.
They have motivations, goals, feelings, ego.
They are lazy and rely on habits.
They don't like being wrong.
They are not good at handing complexity.

Spoiler alert: this is the core slide

Thank you, questions?

Thanks to Hana Dusíková, Pavel Šrámek