Skip to content

Instantly share code, notes, and snippets.

@tstack
Last active November 2, 2021 05:55
Show Gist options
  • Save tstack/74757876c5067e57b8bbc8d5f3e00cf5 to your computer and use it in GitHub Desktop.
Save tstack/74757876c5067e57b8bbc8d5f3e00cf5 to your computer and use it in GitHub Desktop.
Experiment in making a generic builder function for C++ types
/*
* 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