0

I wanted to create a string-container type that checks if the string it contains is valid at construction. In my opinion, it should be preferable if this class has an constructor that takes a std::string as an argument (just to make sure that no implicit type conversions happen).

But when I use this string-container in an std::initializer_list (and even further in a std::unordered_multimap) I get an error, if I define the constructor as explicit. Is there a way to define the constructor as explicit and use it at the same time in a std::multimap? (i.e. a constructor-overload of a magic type I am currently not aware of)

I prepared the following minimal example that should show the point:

#include <utility>
#include <string>
#include <stdexcept>
#include <unordered_map>

class constrained_string_container_t{
    public:
    // variant 1: works
    //constrained_string_container_t(const std::string& inst) : str{inst} {validate();}

    // variant 2: doesn't work
    explicit constrained_string_container_t(const std::string& inst) : str{inst} {validate();}
    explicit constrained_string_container_t(std::string&& inst) : str{std::move(inst)} {validate();}

    // variant 3: works
    //template<typename STR_T>
    //constrained_string_container_t(STR_T&& inst) : str{std::forward<STR_T>(inst)} {validate();}

    // variant 4: doesn't work
    //template<typename STR_T>
    //explicit constrained_string_container_t(STR_T&& inst) : str{std::forward<STR_T>(inst)} {validate();}

    void validate() {if (str == "abc") {throw std::runtime_error{"abc not allowed"};}}

    private:
    std::string str;
};

using pair_t = std::pair<std::string, constrained_string_container_t>;
using init_pair_list_t = std::initializer_list<std::pair<std::string, constrained_string_container_t>>;
using map_t = std::unordered_multimap<std::string, constrained_string_container_t>;

int main() {
    using namespace std::string_literals;

    //suceeds for all variants
    pair_t p0{std::string{"test"}, std::string{"test2"}};

    //fails to compile for variant 2 and variant 4
    init_pair_list_t p1{{std::string{"test"}, std::string{"test2"}}};

    //fails to compile for variant 2 and variant 4
    init_pair_list_t p2{{"test"s, "test2"s}};

    //fails to compile for variant 2 and variant 4
    map_t m{{"test"s, "test2"s}};
}

When I compile it, I get the following errors (with gcc-10):

explicit_shit.cpp: In function ‘int main()’:
explicit_shit.cpp:40:72: error: could not convert ‘{{std::__cxx11::basic_string<char>(((const char*)"test"), std::allocator<char>()), std::__cxx11::basic_string<char>(((const char*)"test2"), std::allocator<char>())}}’ from ‘<brace-enclosed initializer list>’ to ‘init_pair_list_t’ {aka ‘std::initializer_list<std::pair<std::__cxx11::basic_string<char>, constrained_string_container_t> >’}
   40 |         init_pair_list_t p1{{std::string{"test"}, std::string{"test2"}}};
      |                                                                        ^
      |                                                                        |
      |                                                                        <brace-enclosed initializer list>
explicit_shit.cpp:43:48: error: could not convert ‘{{std::literals::string_literals::operator""s(const char*, std::size_t)(4), std::literals::string_literals::operator""s(const char*, std::size_t)(5)}}’ from ‘<brace-enclosed initializer list>’ to ‘init_pair_list_t’ {aka ‘std::initializer_list<std::pair<std::__cxx11::basic_string<char>, constrained_string_container_t> >’}
   43 |         init_pair_list_t p2{{"test"s, "test2"s}};
      |                                                ^
      |                                                |
      |                                                <brace-enclosed initializer list>
explicit_shit.cpp:46:36: error: no matching function for call to ‘std::unordered_multimap<std::__cxx11::basic_string<char>, constrained_string_container_t>::unordered_multimap(<brace-enclosed initializer list>)’
   46 |         map_t m{{"test"s, "test2"s}};
      |                                    ^
1
  • 1
    Well, you wanted to make sure that no implicit type conversions happen, so now you have to make your conversions explicit. As in init_pair_list_t p1{{std::string{"test"}, constrained_string_container_t{std::string{"test2"}}}}; Commented Aug 26, 2022 at 15:08

1 Answer 1

0

In order for this to compile

init_pair_list_t p1{{std::string{"test"}, std::string{"test2"}}};

your type constrained_string_container_t must be implicitly convertible from std::string, since the initialization of the elements of the std::initializer_list from the elements of the braced-init-list will use copy-initialization.

However, as I understand it, your concern is that constrained_string_container_t should not be constructed from something that the compiler might implicitly convert to std::string. So you've marked the constructors explicit. But then the previous thing doesn't work properly.

A possible fix is to have a constructor template that only participates in overload resolution if the argument type is cv std::string:

template <class T>
requires std::same_as<std::remove_cvref_t<T>, std::string>
constrained_string_container_t(T&& inst) : str{std::forward<T>(inst)} {validate();}

See the full example here: https://godbolt.org/z/W791e4rGr

(You should probably also allow classes derived from std::string. How to do this is left as an exercise for the reader.)

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.