Search code examples
c++loopsc++11recursiongcc

Why does the function recursion seem to make the conditions work crazily?


First at all:

  1. test_container, in the following code, is a function that is only used for "debugging" purposes. There is no real use on my working project.

  2. The real function is called foreach (see the last code in this question)

  3. foreach is a function that returns the std::string representation of a container (after this fix, I'll make it work with int arrays too)

  4. Even the foreach representation here is minimalized for better understanding: It looks here like the test_container, with a bit of differences.


So, I have this code:

#include <iostream>
#include <map>
#include <sstream>
#include <type_traits>
#include <vector>

template <typename T>
struct has_template_argument {
    static const bool value = false;
};

// Specialization of a template for types with template arguments
template <template <typename...> class Container, typename T>
struct has_template_argument<Container<T>> {
    static const bool value =
        !std::is_same<std::basic_string<T>, Container<T>>::value;
};

// Function to verify if the variable has a template argument
template <typename T>
bool has_template_arg(const T&) {
    return has_template_argument<T>::value;
}

// The test collection funtion.
template <typename T>
std::string test_container(const T& container) {
    std::stringstream listn;

    listn << "{";

    if (has_template_arg(container)) {
        auto it = container.begin();

        if (it != container.end()) {
            if (!has_template_arg(*it)) {
                listn << *it;

                std::cout << "\n";
            } else {
                // It's here that the problem starts.

                listn << test_container(*it);

                std::cout << "\nThis container is multi-dimentioned";
            }
            ++it;
        }

    } else {
        std::cout << "\nThis variable has no container to \"iterate\"";
    }

    listn << "}";

    return listn.str();
}

Explanation:

The test_container function first tries to see if the passed argument has a template argument.

To do so, it uses the has_template_arg function. has_template_arg returns True for everything that has <>, except multiple template arguments (for now), and strings (expected, as strings SHOULD NOT be considered containers)

The std::cout's within the function serves solely to demonstrate that the function normally falls into a condition ( if (!has_template_arg(*it)) ); however, when there is a recursive call to the present function, it ignores the conditions and performs the recursion erroneously.

This is a demonstration that the problem, apparently, is not in the has_template_arg checker:

std::map<int, int> mep = {{1, 2}, {4, 5}};
std::vector<std::vector<int>> vet = {{1, 2, 3}, {4, 5, 6}};
std::vector<std::string> strVet = {"hello", "world"};
std::string str = "Hi";
const char* chr = "Some char here";

// Works perfectly:
std::cout << "Is Vector container?: "      << has_template_arg(strVet) << std::endl;
std::cout << "Is Multivector container?: " << has_template_arg(vet)    << std::endl;

// This below CURRENTLY does not work: No problem for now, can wait.
std::cout << "Is Map container?: "         << has_template_arg(mep)    << std::endl;

// Also works:
std::cout << "Is Int container?: "        << has_template_arg(5)    << std::endl;
std::cout << "Is Char container?: "       << has_template_arg('o')  << std::endl;
std::cout << "Is Char[] container?: "     << has_template_arg("oi") << std::endl;
std::cout << "Is Const char container?: " << has_template_arg(chr)  << std::endl;
std::cout << "Is String container: "      << has_template_arg(str)  << std::endl;

std::cout    << "Vector Recursion: "       << test_container(strVet)  << std::endl;

// std::cout << "String Recursion: "       << test_container(str)     << std::endl;
// std::cout << "Multivector recursion: "  << foreach(vet)            << std::endl;
// std::cout << "Const char recursion: "   << test_container(chr)     << std::endl;

Important note: If you comment out the line where the code is going into recursion:

// listn << test_container(*it);

everything will work correctly.

However, it will not be able to check whether the passed argument should also be "iterated" or should be sent to the string stream, as desired.

On the other hand, if you don't (comment), the following compilation errors are:

  1. If you are only passing Vector Recursion:
std::cout    << "Vector Recursion: "       << test_container(strVet)   << std::endl;

// std::cout << "String Recursion: "       << test_container(str)      << std::endl;

// std::cout << "Multivector recursion: "  << foreach(vet)             << std::endl;

// std::cout << "Const char recursion: "   << foreach(chr)             << std::endl;

This is the result:

Compiling single file...

--------

- Filename: C:\Users\user\Workstation\C\CPP-Projects\TesteCPP\Testes\ephemeral.cpp

- Compiler Name: TDM-GCC 4.9.2 64-bit Release



Processing C++ source file...

--------

- C++ Compiler: C:\Program Files (x86)\Dev-Cpp\MinGW64\bin\g++.exe

- Command: g++.exe [...]



C:\Users\user\Workstation\C\CPP-Projects\TesteCPP\Testes\ephemeral.cpp: In instantiation of 'std::string test_container(const T&) [with T = char; std::string = std::basic_string<char>]':

[...]\ephemeral.cpp:42:41:   recursively required from 'std::string test_container(const T&) [with T = std::basic_string<char>; std::string = std::basic_string<char>]'

[...]\ephemeral.cpp:42:41:   required from 'std::string test_container(const T&) [with T = std::vector<std::basic_string<char> >; std::string = std::basic_string<char>]'

[...]\ephemeral.cpp:100:60:   required from here

[...]\ephemeral.cpp:35:29: error: request for member 'begin' in 'container', which is of non-class type 'const char'

   auto it = container.begin();

                             ^



[...]\ephemeral.cpp:36:10: error: request for member 'end' in 'container', which is of non-class type 'const char'

   if (it != container.end()) {

          ^
  1. If you are only passing the '"String Recursion: " (...)':
// std::cout << "Vector Recursion: "       << test_container(strVet)    << std::endl;

std::cout    << "String Recursion: "       << test_container(str)       << std::endl;

// std::cout << "Multivector recursion: "  << foreach(vet)     << std::endl;

// std::cout << "Const char recursion: "   << test_container(chr)       << std::endl;

That's what you get:

Compiling single file...

--------

- Filename: C:\Users\user\Workstation\C\CPP-Projects\TesteCPP\Testes\ephemeral.cpp

- Compiler Name: TDM-GCC 4.9.2 64-bit Release



Processing C++ source file...

--------

[...]

[...]\ephemeral.cpp: In instantiation of 'std::string test_container(const T&) [with T = char; std::string = std::basic_string<char>]':

[...]\ephemeral.cpp:41:40:   required from 'std::string test_container(const T&) [with T = std::basic_string<char>; std::string = std::basic_string<char>]'

[...]\ephemeral.cpp:100:51:   required from here

[...]\ephemeral.cpp:35:29: error: request for member 'begin' in 'container', which is of non-class type 'const char'

   auto it = container.begin();

                             ^



C:\Users\user\Workstation\C\CPP-Projects\TesteCPP\Testes\ephemeral.cpp:36:10: error: request for member 'end' in 'container', which is of non-class type 'const char'

   if (it != container.end()) {

          ^

And here is the foreach:

// This is the "real" code I said (that also needs fix, although it works):

template <typename T>
std::string foreach(const T& collection) {
    std::stringstream listn;
    listn << "{";

    auto it = collection.begin();
    
    if (it != collection.end()) {
        if (has_template_arg(*it)) {
            // listn << foreach(*it);
        } else {
          listn << *it;
      }
        ++it;
    }

    while (it != collection.end()) {
        listn << "," << *it;
        ++it;
    }
    listn << "}";

    return listn.str();
}

I've tried with multiple versions of GNU C++ Compiler, and they all returned the same error message, although with some differences.

It means, then, that this is a simple syntax error - which is known where it begins, but is not known how and the way to fix it.

Expected: The GCC g++ compiles the code, with no errors or warnings. This means that is desired the test_container function to know the time to make the recursion work inside them.


Solution

  • Since you are using C++11, you can't solve the issue with the non-taken branch being invalid with if constexpr, which would otherwise be my suggestion.

    Instead, you could create different versions of your foreach function and use SFINAE to make sure the correct version is selected.

    I'd replace the type trait that you now have with one that checks if the supplied argument supports std::begin()/std::end(). I'd also add one type trait to check if the supplied argument supports streaming to an ostream.

    #include <type_traits>
    #include <utility>
    
    // void_t from c++20
    // Early version to support old compilers that haven't implemented
    // https://cplusplus.github.io/CWG/issues/1558.html
    template<typename... Ts>
    struct make_void { typedef void type; };
     
    template<typename... Ts>
    using void_t = typename make_void<Ts...>::type;
    //----------------------------------------------------------------
    // trait to check if T supports std::begin and std::end
    template <class, class = void>
    struct has_begin_and_end : std::false_type {};
    
    template <class T>
    struct has_begin_and_end<T, void_t<decltype(std::begin(std::declval<T&>()),
                                                std::end(std::declval<T&>()))>>
        : std::true_type {};
    
    // trait to check if T supports streaming to an ostream
    template <class, class = void>
    struct can_ostream : std::false_type {};
    
    template <class T>
    struct can_ostream<
        T, void_t<decltype(std::declval<std::ostream&>() << std::declval<T>())>>
        : std::true_type {};
    

    Your foreach versions could then look like this:

    // char[N] version - trims off `\0` if there is one
    template <std::size_t N>
    std::string foreach (const char (&arr)[N]) {
        return {arr, arr + N - (arr[N - 1] == '\0')};
    }
    
    // std::string version, adds quotes around the string
    std::string foreach (const std::string& str) {
        return '"' + str + '"';
    }
    
    // forward declaration of the std::pair version
    template <class F, class S>
    std::string foreach (const std::pair<F, S>& pair);
    
    // generic other non-container versions that requires that T can be streamed
    // and that T does _not_ support std::begin/std::end
    template <class T>
    typename std::enable_if<can_ostream<T>::value && !has_begin_and_end<T>::value,
                            std::string>::type
    foreach (const T& var) {
        std::ostringstream listn;
        listn << var;
        return listn.str();
    }
    
    template <class T>  // container version:
    typename std::enable_if<has_begin_and_end<T>::value,
                            std::string>::type
    foreach (const T& container) {
        std::ostringstream listn;
        listn << '{';
        auto it = std::begin(container);
        auto end = std::end(container);
        if (it != end) {
            listn << foreach (*it);
            for (++it; it != end; ++it) {
                listn << ", " << foreach (*it);
            }
        }
        listn << '}';
        return listn.str();
    }
    
    template <class F, class S>  // pair version
    std::string foreach (const std::pair<F, S>& pair) {
        return '{' + foreach (pair.first) + '=' + foreach (pair.second) + '}';
    }
    

    You can see it working in this demo.