-
Notifications
You must be signed in to change notification settings - Fork 10
API: CPP API: Naming and Signature of Memory Access Methods
The page concerns C++ methods that handle big chunks of memory, and by big here it means dynamically allocated memory. The goal of specified name patterns is to improve clarity---a user can simply look at the name and know its intention. The goal of specified signature patterns is to improve safety and efficiency---the program will faithfully fulfill the programmer's intention and not leave any chance to introduce memory bugs/confusions/inefficiency, such as dangling pointers, unclear ownership, and unnecessary copies.
In the following specifications,
notice the use of const
, smarter pointers, rvalue references,
and choice of verbs.
This is when a large chunk of memory is passed into a function. This function can either simply read the memory block and leave its ownership to the caller, or take the ownership of this memory block.
- Read only a memory block.
// Do
auto copy_something(const float* buf, size_t len) -> status;
auto read_something(const std::vector<float>& buf) -> status;
auto view_something(const std::unique_ptr<float[]>& buf, size_t len) -> status;
// Do Not
auto copy_something(float* buf, size_t len) -> status; // Is buf going to be changed?
auto read_something(std::vector<float>& buf) -> status; // Is buf going to be changed?
- Take the ownership of the input data block:
// Do
auto take_something(std::vector<float>&& buf) -> status; // Use rvalue reference
auto take_something(std::unique_ptr<float[]>&& buf, size_t len) -> status; // Use rvalue reference
// Do Not
auto take_something(float* buf, size_t len) -> status; // Ownership unclear
auto take_something(std::vector<float> buf) -> status; // Unnecessary copy
This is when the function produces results in a memory block, and communicates these results to the caller. There are three cases with regard to the ownership of the memory block.
- Provide read-only access to a block of memory that this function holds.
// Do
auto view_something() const -> const std::vector<float>&;
auto view_something() const -> std::pair<const float*, size_t>;
auto view_something() const -> std::pair<const std::unique_ptr<float[]>&, size_t>;
// Do Not
auto view_something() -> const std::vector<float>&;
auto view_something() const -> std::vector<float>&;
auto view_something() const -> std::pair<float*, size_t>; // More than one entities hold the raw pointer.
- Provide a copy of a block of memory that the class holds, so the caller will have its own copy.
// Do
auto get_something() const -> std::vector<float>;
auto get_something() const -> std::pair<std::unique_ptr<float[]>, size_t>;
// Do Not
auto get_something() -> std::vector<float>;
auto get_something() const -> std::pair<float*, size_t>; // More than one entities hold the raw pointer.
Note on the suggested usage that returns a big object:
auto get_something() const -> std::vector<float>;
It will not incur an unnecessary creation of a temporary object, due to RVO (return value optimization). The programmer, however, does need to pay attention to not invalid RVO by selecting the object to return based on runtime conditions.
// Do
auto get_something() const -> std::vector<float>
{
auto var = std::vector<float>(128, 0.0);
/* Do something with var */
return var;
}
// Do Not
auto get_something() const -> std::vector<float>
{
auto var1 = std::vector<float>(128, 0.0);
auto var2 = std::vector<float>(128, 0.0);
/* Do something with var1 and var2 */
if( condition ) return var1;
else return var2;
// Explanation: which variable to return has to be determined at runtime,
// not compile time. Thus, the compiler is not able to perform any RVO.
}
- Release the ownership of a chunk of memory that the class holds and the caller is expected to hold the ownership.
// Do
auto release_something() -> std::vector<float>&&; // Use rvalue reference
auto release_something() -> std::pair<std::unique_ptr<float[]>, size_t>;
// Do Not
auto release_something() -> std::vector<float>; // Unnecessary copy
auto release_something() -> std::pair<float*, size_t>; // Ownership unclear
While SPERR prefers the former two approaches of memory access, there are cases where a method both reads and writes to a memory block that the caller holds. The golden rule in this case is that the caller is responsible of allocating sufficient memory and managing it during its entire lifetime. The input+output method only reads and writes to that memory, but does not do anything else (e.g., de-allocate memory, allocate a new chunk of memory, re-size a container). Of course, the method needs to be clear in documenting how much memory it expects.
// Do
auto fill_values( std::vector<float>& buf ) -> status; // buf is expected to be as long as NX*NY*NZ
Lossy Scientific data compression with SPERR