Last active
November 2, 2021 05:55
-
-
Save tstack/74757876c5067e57b8bbc8d5f3e00cf5 to your computer and use it in GitHub Desktop.
Experiment in making a generic builder function for C++ types
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* An experiment in creating a generic builder for C++. | |
* | |
* This is inspired by the following post: | |
* | |
* https://marcoarena.wordpress.com/2021/10/25/the-self-growing-builder/ | |
* | |
* The changes from the post are that the tag structs are used to hold | |
* the argument values and there is a single generic builder function | |
* that accepts the tag structs. These changes remove a lot of the | |
* redundant code with some tradeoffs, like inferior compile-time error | |
* messages. | |
* | |
* Usage: | |
* | |
* The obj_builder::builder class provides a static function that | |
* constructs an object based off the "named" arguments, like so: | |
* | |
* Configuration::Builder::build( | |
* Configuration::With::name{"test"}, | |
* Configuration::With::folder{"/tmp"}, | |
* ... other arguments here ... | |
* ); | |
* | |
* The builder class is parameterized by the object type and the | |
* list of "named" parameters, which are structs containing a | |
* "value" field that will be passed to the object initializer. | |
* So, the "Builder" name in the above example is simply a "using" | |
* declaration that specializes the builder template. | |
*/ | |
#include <iostream> | |
#include <string> | |
#include <filesystem> | |
#include <type_traits> | |
#include <variant> | |
namespace obj_builder | |
{ | |
namespace details | |
{ | |
template <typename Req> | |
constexpr bool contains_tag() | |
{ | |
return false; | |
} | |
/** | |
* Check if the type Req is in the Tags parameter pack. | |
*/ | |
template <typename Req, typename Tag, typename... Tags> | |
constexpr bool contains_tag() | |
{ | |
if constexpr (std::is_base_of<Req, Tag>()) | |
{ | |
return true; | |
} | |
else | |
{ | |
return contains_tag<Req, Tags...>(); | |
} | |
} | |
/** | |
* Check that the given argument is in the list of parameters. | |
*/ | |
template <typename Arg, typename... Params> | |
constexpr void ensure_arg_in_params() | |
{ | |
static_assert(contains_tag<Arg, Params...>(), | |
"Argument is NOT found in the builder's parameter list"); | |
} | |
/** | |
* @return The default value for this parameter type. | |
*/ | |
template <typename UnspecifiedParameter> | |
auto get_arg_for_param() -> decltype(UnspecifiedParameter::value) | |
{ | |
return std::move(UnspecifiedParameter().value); | |
} | |
/** | |
* @tparam Parameter The parameter type to retrieve from the arguments. | |
* @param args The arguments to search through for the given Parameter type. | |
* @return The value from the argument matching the given parameter or the | |
* default value from the Parameter type, if there is one. | |
*/ | |
template <typename Parameter, typename Arg, typename... Args> | |
auto get_arg_for_param(Arg &arg, Args &...args) -> decltype(Parameter::value) | |
{ | |
if constexpr (std::is_base_of<Arg, Parameter>::value) | |
{ | |
static_assert(!contains_tag<Parameter, Args...>(), | |
"This builder argument cannot be passed multiple times"); | |
return std::move(arg.value); | |
} | |
else | |
{ | |
return get_arg_for_param<Parameter>(args...); | |
} | |
} | |
} | |
/** | |
* A generic builder for types. | |
* | |
* @tparam T The type of object to construct. | |
* @tparam Params The parameter types used to construct the object. | |
*/ | |
template <typename T, typename... Params> | |
struct builder | |
{ | |
/** | |
* Builds the object from the given parameters. | |
* | |
* @param args The arguments to the builder. | |
* @return The newly constructed object. | |
*/ | |
template <typename... Args> | |
[[nodiscard]] static T build(Args... args) | |
{ | |
(details::ensure_arg_in_params<Args, Params...>(), ...); | |
return init_from_values(details::get_arg_for_param<Params>(args...)...); | |
} | |
private: | |
/** | |
* Initialize the type from the given values. | |
* | |
* @param values The values to pass to the type initializer. | |
* @return The newly constructed object. | |
*/ | |
template <typename... Values> | |
static T init_from_values(Values... values) | |
{ | |
return T{std::move(values)...}; | |
} | |
}; | |
} | |
/** | |
* An example configuration class. | |
*/ | |
class Configuration | |
{ | |
public: | |
struct CustomChannelInfo | |
{ | |
std::string ip; | |
int port; | |
std::string symbol; | |
}; | |
using DbConnectionInfo = std::string; | |
using FilesystemInfo = std::filesystem::path; | |
using Storage = std::variant<DbConnectionInfo, FilesystemInfo, CustomChannelInfo>; | |
/** | |
* Container for the parameters that can be passed to the builder. | |
*/ | |
struct With | |
{ | |
struct name | |
{ | |
std::string value{"Untitled"}; // parameter with a default value | |
}; | |
struct folder | |
{ | |
folder() = delete; // delete the default constructor to make this a required parameter | |
std::filesystem::path value; | |
}; | |
struct storage | |
{ | |
storage() = delete; | |
Storage value; | |
}; | |
}; | |
using Builder = obj_builder::builder<Configuration, With::name, With::folder, With::storage>; | |
const std::string _name; | |
const std::filesystem::path _folderPath; | |
const Storage _storage; | |
}; | |
int main(int args, char *argv[]) | |
{ | |
#if !defined(USE_INIT) | |
auto c1 = Configuration::Builder::build( | |
Configuration::With::folder{"/tmp"}, | |
Configuration::With::storage{Configuration::DbConnectionInfo{"connection::string"}}); | |
#else | |
auto c1 = Configuration{ | |
"Untitled", | |
"/tmp", | |
Configuration::DbConnectionInfo{"connection::string"}}; | |
#endif | |
#if defined(UNUSED_ARG) | |
// Passing an unrecognized parameter type will cause compilation to fail | |
auto c2 = Configuration::Builder::build( | |
Configuration::With::folder{"/tmp"}, | |
Configuration::DbConnectionInfo{"connection::string"}); | |
#endif | |
#if defined(REQUIRED_MISSING) | |
// Not passing a required parameter, "folder" in this case, will cause compilation to fail | |
auto c3 = Configuration::Builder::build( | |
Configuration::With::storage{Configuration::DbConnectionInfo{"connection::string"}}); | |
#endif | |
std::cout << "Name: " << c1._name << std::endl | |
<< "Folder: " << c1._folderPath << std::endl; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment