Search code examples
c++c++17initializer-list

Heterogeneous initializer list


I have a QueryField and Select helper classes used to construct SQL statements:

class QueryField
{
public:
    QueryField(std::string_view column)
        : m_column{ column }
    {
    }
    QueryField(std::string_view column, std::string_view alias)
        : m_column{ column }
        , m_alias{ alias }
    {
    }

private:
    std::string m_column;
    std::string m_alias;
};

class Select
{
public:

    Select(std::initializer_list<QueryField> fields)
    {
        for (auto & field : fields)
        {
            m_fields.emplace_back(std::move(field));
        }
    }

private:

    std::vector<QueryField> m_fields;
};

as seen from the code above Select is a collection of QueryField objects that can initialized like this:

Select{ QueryField{ "up.audit_option" "option" }, QueryField("uep.success"), QueryField("uep.failure") };

is it possible to eliminate the need of specifying QueryField explicitly and initialize Select object as follows?

Select{ { "up.audit_option" "option" }, "uep.success", "uep.failure" };

Solution

  • With your solution, you can indeed drop the types, but you have to keep the braces:

    Select{ { "up.audit_option" "option" }, {"uep.success"}, {"uep.failure"} }
    

    Also be careful with initialized list: all the elements inside will be copied. Even if you move:

    Select(std::initializer_list<QueryField> fields)
    {
        for (auto & field : fields)
        {
            // Actually copy. No move is done.
            m_fields.emplace_back(std::move(field));
        }
    }
    

    No move is allowed since every elements in the initializer list are constant.


    My preferred solution would be to drop std::initializer_list and be simple with simple case and more explicit with complex cases.

    To allow true heterogenous parameters, I'll go with variadic templates:

    template<typename... Args>
    Select(Args&&... fields) :
        m_fields{QueryField{std::forward<Args>(args)}...} {}
    

    If you want to keep the copy/move constructor, you must filter out some parameter types:

    template<typename T, typename = void typename... Args>
    struct is_not_copy_impl : std::false_type {};
    
    template<typename T, typename Arg>
    struct is_not_copy_impl<T, std::enable_if_t<std::is_base_of_v<T, std::decay_t<Arg>>>, Arg> : std::true_type {};
    
    template<typename T, typename... Args>
    using is_not_copy = is_not_copy_impl<T, void, Args...>;
    
    template<typename... Args, std::enable_if_t<!is_not_copy<Select, Args...>::value>* = nullptr>
    Select(Args&&... fields) :
        m_fields{QueryField{std::forward<Args>(args)}...} {}
    

    This code will move when a QueryField is passed, and construct a new one when a value of other type is passed.

    The usage is this:

    Select{
        QueryField{"up.audit_option" "option"},
        "uep.success",
        "uep.failure"
    };