-
Notifications
You must be signed in to change notification settings - Fork 396
Development Tips and Resources
It is normally fastest to pass objects larger than the pointer size (64-bits on a 64-bit OS) by reference and small, built-in types like int and double by value, such as in void foo( std::string const & name, int const age );
Passing large objects by value causes expensive copy operations on every call: this is a common performance bug made by developers new to C++.
There are some exceptions to this:
- Proxy objects that need to be created at the time of a function call are always pass-by-value.
- If you will be creating a copy from the argument object passing it by value can save writing the copy construction declaration: this should be weighed against the concern that passing large objects by value may cause users of the function.
-
std::initializer_list
looks like a container but doesn't hold its own data and is therefore small and best passed by value.
C style arrays and strings require careful memory management and don't provide any bounds checking so they expose code to bugs. Even if you use them correctly the next person to modify that code can too easily break it.
C++ provides std::string
and safe array-like data structures in addition to the ObjexxFCL FArrays, that handle memory allocation and release and most of them perform bounds checking in debug builds. Release builds should have essentially the same performance as the C equivalents for most purposes.
If you need C style arrays or strings for use with an external library it is a good idea to use safe wrappers that auto-convert to the C arrays or strings but handle the memory allocation and bounds checking, such as the CArray and Cstring types in the ObjexxFCL.
Don't try to have global/namespace or class static constant values dependent on values defined in a different file. C++ does not guarantee the order in which such constants are defined in different files at startup. While such dependent values may work on some builds/compilers they will probably fail on others. Compilers don't yet detect such usage but PC-lint/Flexelint are claimed to find them so running this as part of the CI testing process is a good idea. If you can't define all related constants in one file then either use local versions of the needed dependencies or turn the constant into a static method with a local static constant to work around the issue.
The order in which class members are initialized is determined by their order of declaration within the class, not the order in which their initializers appear in a constructor's initialization list. If there are interdependencies in the member initialization this can be important.
The new C++11 move constructors are very useful for performance, especially for larger objects that own heap-allocated data or other expensive resources. Understanding what they are, when the default move constructors are provided and suffice, and how to write them is worth the effort.
Below are some tips that are specific to EnergyPlus, either due to its coding usage or the conversion from Fortran.
The ObjexxFCL Optional mechanism emulates the semantics of Fortran OPTIONAL arguments, which were widely used in EnergyPlus. This is a valid and safe technique, similar to Boost.Optional, that can be useful in limited scenarios when a complex pattern of optional arguments is needed. There is some runtime overhead to create the Optional wrapper objects on each function call so it can be a performance problem in high call count functions. There is also a small compile time overhead, code size impacts, and the issue of using a tool that is not going to be familiar to general C++ developers. For these reasons it is recommended that we gradually move away from Optional usage in EnergyPlus except perhaps for some limited situations where it is substantially simplifying code.
- We should migrate away from Optional, starting with places where it can be easily replaced by overloading or C++ default arguments.
- The large number of optional arguments in some EnergyPlus interfaces are a Fortran-ism that should get better as a side effect of OO migration so it may be best to leave them alone until a module is refactored to OO rather than doing the large job of replacing them in one gulp.
- Continuing to use Optional where it is already in use is not terrible for now but is best avoided in new interfaces/modules.
- If the function can operate correctly with a default value for the argument then C++ default argument usage is preferred. The default would have to be either an OK value to use or an out of normal domain "sentinel" value that means "nothing passed". C++ default arguments can be intermixed with Optional arguments at the end of an argument list so a function does not have to be fully migrated all at once and new arguments can be added with C++ style.
- Default argument values should avoid expensive construction such as
std::string const & s = ""
- If there is no "sentinel" value that can be used to signal "nothing passed" a separate boolean sentinel argument could be paired with the value (or combined in a std::pair). It could be argued that this is not better than Optional.
- If there are only 1 or 2 arguments that may or may not be present overloaded functions should be considered instead of default arguments.