gcc и clang не выдают соответствующий вызов функции, но msvc (cl) компилируется и работает должным образом

Я написал небольшой шаблон функции, который объединяет разные контейнеры в новый контейнер:

#include <vector>
#include <unordered_set>
#include <string>
#include <iostream>
#include <iterator>

namespace impl
{
    template <typename OutIterator, typename Container, typename ...Containers>
    void join(OutIterator iterator, const Container& container, const Containers& ...containers)
    {        
        for (const auto& item : container)
            *iterator++ = item;

        join(iterator, containers...);  // gcc and clang cannot resolve this call
    }

    template <typename OutIterator, typename Container>
    void join(OutIterator iterator, const Container& container)
    {        
        for (const auto& item : container)
            *iterator++ = item;
    }
}

template <typename OutContainer, typename ...Containers>
OutContainer join(const Containers& ...containers)
{
    OutContainer container;
    auto it = std::inserter(container, container.end());
    impl::join(it, containers...);
    return container;
}

int main()
{
    using namespace std;
    vector<string> a = {"one"s, "two"s, "three"s};
    unordered_set<string> b = {"four"s, "five"s };
    auto res = join<unordered_set<string>>(a, b);

    for (auto& i : res)
        cout << i << "\n";

    return 0;
}

При использовании MSVC (cl.exe) с /std:c++17 код компилируется и работает нормально. Но при компиляции с clang-6.0 или gcc-7.3 выдается ошибка компилятора. т.е. gcc говорит

no matching function for call to 'join(std::insert_iterator<std::unordered_set<std::__cxx11::basic_string<char> > >&)'

Таким образом, очевидно, что функция с этой сигнатурой не определена. Но я не понимаю, почему он пытается вызвать такую ​​функцию. Разве это не должно быть решено так

// in main()
join<unordered_set<string>>(a, b);

unordered_set<string> join(const vector<string>& a, const unordered_set<string>& b);
void impl::join(std::insert_iterator<unordered_set<string>> iterator, const vector<string>& a, const unordered_set<string>& b);
void impl::join(std::insert_iterator<unordered_set<string>> iterator, const unordered_set<string>& b);

Почему gcc пытается создать экземпляр join(std::insert_iterator<std::unordered_set<std::__cxx11::basic_string<char>>>&)?

Вот пример использования проводника компилятора.


person Timo    schedule 03.04.2018    source источник
comment
Еще одна приятная вещь в их переупорядочивании заключается в том, что вы можете избежать дублирования логики (цикл for).   -  person StoryTeller - Unslander Monica    schedule 03.04.2018
comment
Это очень опасная реализация, те же проблемы, что и у старых добрых функций C, таких как strcat! Что, если выходной итератор достигнет конечного итератора целевого контейнера???   -  person Aconcagua    schedule 03.04.2018
comment
@Aconcagua - выходной итератор является средством вставки. Я предполагаю, что эти две функции находятся в пространстве имен impl, поэтому они вообще опасны.   -  person StoryTeller - Unslander Monica    schedule 03.04.2018
comment
@Aconcagua хорошо, функции в пространстве имен impl предназначены для использования только из функции глобальной области видимости join, которая использует insert_iterator, что означает, что она вставляет в контейнер каждое назначение.   -  person Timo    schedule 03.04.2018
comment
ОК... Лично я бы все равно отразил это в параметрах шаблона. Может быть, я немного фанатик безопасности? Примечание: Немного короче: auto it = std::back_inserter(container);...   -  person Aconcagua    schedule 03.04.2018
comment
@Aconcagua, как бы вы отразили это в параметрах шаблона? Я не могу использовать back_inserter, потому что контейнер может быть (как показано в моем примере) чем-то другим, кроме вектора (например, набором), у которого нет функции push_back. back_inserter_iterator и insert_iterator используют разные механики.   -  person Timo    schedule 03.04.2018
comment
@Тимо template <typename OutContainer, typename Container> void join(std::insert_iterator<OutContainer> i, Container const& c);; back_inserter: извините, присматривал за необходимостью большей гибкости... Хотя в других ситуациях могло бы быть неплохо.   -  person Aconcagua    schedule 03.04.2018


Ответы (1)


gcc и clang верны. У MSVC все еще есть проблемы с поиском правильного имени шаблона (т.е. «двухэтапный поиск»).

join в join(iterator, containers...) является зависимым именем. Кандидатами на поиск этого имени являются:

  • Все имена в точке определения шаблона. Этот поиск просто находит сам себя (перегрузка с переменным числом аргументов), а не другую перегрузку (с двумя аргументами), потому что она еще не объявлена.
  • Все имена, которые ADL может найти в своих аргументах. Ни один из этих аргументов не имеет impl в качестве связанного пространства имен, поэтому другая перегрузка также не будет найдена.

В этой ситуации исправление тривиально: просто переупорядочите две перегрузки join(). Это гарантирует, что 2-arg join() будет найден по первому пункту списка.


Обратите внимание, что в C++17 вам даже не нужны две перегрузки. Достаточно одного:

template <typename OutIterator, typename Container, typename... Containers>
void join(OutIterator iterator, Container const& container, Container const&... containers) {
    for (auto const& item : container) {
        *iterator++ = item;
    }

    if constexpr(sizeof...(Containers) > 0) {
        join(iterator, containers...);
    }
}

Также рассмотрите возможность использования std::copy() вместо цикла. Что на самом деле позволит:

template <typename OutIterator, typename... Containers>
void join(OutIterator iterator, Container const&... containers) {
    using std::begin;
    using std::end;
    (iterator = std::copy(begin(containers), end(containers), iterator), ...);
}
person Barry    schedule 03.04.2018
comment
MSVC демонстрирует правильное поведение, если вы используете флаг /permissive- (находится в узле c/c++-›language настроек проекта, помеченном как режим соответствия) - person SoronelHaetir; 03.04.2018