string_view
operator+
vs. StrCat()
absl::Status
std::bind
absl::optional
and std::unique_ptr
absl::StrFormat()
make_unique
and private
Constructors.bool
explicit
= delete
)switch
Statements Responsibly= delete
AbslHashValue
and Youcontains()
std::optional
parametersif
and switch
statements with initializersinline
VariablesStatusOr
std::unique_ptr
Must Be MovedAbslStringify()
Originally posted as TotW #173 on December 19, 2019
By John Bandela
Updated 2020-04-06
Quicklink: abseil.io/tips/173
It came without packages, boxes or bags. And he puzzled and puzzled ‘till his puzzler was sore.
-Dr. Seuss
Designated initializers are a C++20 feature that is available in most compilers today. Designated initializers make using option structs easier and safer since we can construct the options object in the call to the function. This results in shorter code and avoids a lot of temporary lifetime issues with option structs.
struct PrintDoubleOptions { absl::string_view prefix = ""; int precision = 8; char thousands_separator = ','; char decimal_separator = '.'; bool scientific = false; }; void PrintDouble(double value, const PrintDoubleOptions& options = PrintDoubleOptions{}); std::string name = "my_value"; PrintDouble(5.0, {.prefix = absl::StrCat(name, "="), .scientific = true});
For more background on why option structs are helpful and the potential pitfalls in using them that designated initializers help avoid, read on.
Functions that take many arguments can be confusing. To illustrate, let us consider this function for printing out a floating point value.
void PrintDouble(double value, absl::string_view prefix, int precision, char thousands_separator, char decimal_separator, bool scientific);
This function provides us a lot of flexibility because it takes so many options.
PrintDouble(5.0, "my_value=", 2, ',', '.', false);
The above code will print out: “my_value=5.00”.
However, it is hard to read this code and know to which parameter each argument
corresponds. For instance, here we have inadvertently mixed up the order of our
precision
and thousands_separator
.
PrintDouble(5.0, "my_value=", ',', '.', 2, false);
Historically, we have used argument comments to clarify argument meanings at call sites to reduce this sort of ambiguity. The addition of argument comments to the above example would allow ClangTidy to detect the error:
PrintDouble(5.0, "my_value=", /*precision=*/2, /*thousands_separator=*/',', /*decimal_separator=*/'.', /*scientific=*/false);
However, argument comments still have several drawbacks:
=
sign) can disable the check entirely with no
warning, providing a false sense of security.No matter whether your arguments are commented or not, specifying lots of options can also be tedious. Many times there are sensible defaults for the options. To address this concern, we can add defaults to the parameters.
void PrintDouble(double value, absl::string_view prefix = "", int precision = 8, char thousands_separator = ',', char decimal_separator = '.', bool scientific = false);
Now we can call PrintDouble
with less boilerplate.
PrintDouble(5.0, "my_value=");
However, if we want to specify a non-default argument for scientific
, we would
still be forced to specify values for all of the parameters that come before it:
PrintDouble(5.0, "my_value=", /*precision=*/8, // unchanged from default /*thousands_separator=*/',', // unchanged from default /*decimal_separator=*/'.', // unchanged from default /*scientific=*/true);
We can address all of these issues by grouping all of the options together in an option struct:
struct PrintDoubleOptions { absl::string_view prefix = ""; int precision = 8; char thousands_separator = ','; char decimal_separator = '.'; bool scientific = false; }; void PrintDouble(double value, const PrintDoubleOptions& options = PrintDoubleOptions{});
Now we can have names for our values, as well as flexibly use defaults.
PrintDoubleOptions options; options.prefix = "my_value="; PrintDouble(5.0, options);
There are some issues with this solution, though. First is that we now have some extra boilerplate in passing options. The second is that this style is more at risk for temporary lifetime issues.
For example, when we took all the options as parameters the following code was safe:
std::string name = "my_value"; PrintDouble(5.0, absl::StrCat(name, "="));
In the code above, we are creating a temporary string
and binding a
string_view
to that. The temporary lifetime is the duration of the function
call so we are safe, but using an options struct in the same manner, results in
a dangling string_view
.
std::string name = "my_value"; PrintDoubleOptions options; options.prefix = absl::StrCat(name, "="); PrintDouble(5.0, options);
There are two ways we can fix this. The first is to simply change the type of
prefix
from string_view
to string
. The downside of doing this is that now
the option struct is less efficient than directly passing the arguments. The
other way that we can fix this is to add setter member functions.
class PrintDoubleOptions { public: PrintDoubleOptions& set_prefix(absl::string_view prefix) { prefix_ = prefix; return *this; } absl::string_view prefix() const { return prefix_; } // Setters and getters for the other member variables. private: absl::string_view prefix_ = ""; int precision_ = 8; char thousands_separator_ = ','; char decimal_separator_ = '.'; bool scientific_ = false; };
This can then be used to set the variables in the call.
std::string name = "my_value"; PrintDouble(5.0, PrintDoubleOptions{}.set_prefix(absl::StrCat(name, "=")));
As you can see, the cost is that our option struct became a more complicated class with a lot more boilerplate.
The simpler alternative is to use designated initializers as shown at the top.
Using std::make_unique
with designated initializers requires explicitly
mentioning the options struct type, or creating a helper factory function. (This
works around a limitation of “perfect forwarding”, that only values with known
types can be forwarded.)
class DoublePrinter { explicit DoublePrinter(const PrintDoubleOptions& options); static std::unique_ptr<DoublePrinter> Make(const PrintDoubleOptions& options); ... }; auto printer1 = std::make_unique<DoublePrinter>( PrintDoubleOptions{.scientific=true}); auto printer2 = DoublePrinter::Make({.scientific=true});
For functions which take multiple arguments which may be confused by the caller or where you want to specify default arguments without having to worry about the order, strongly consider using option structs to increase both convenience and code clarity.
When calling functions that take option structs, using designated initializers can result in shorter code as well as potentially avoiding temporary lifetime issues.
Designated initializers by virtue of their conciseness and clarity further tip the balance towards preferring functions that take option structs over those that have many parameters.