3

C++20 std::span is a very nice interface to program against. But there doesn't seem to be an easy way to have a span of spans. Here's what I am trying to do:

#include <iostream>
#include <span>
#include <string>
#include <vector>

void print(std::span<std::span<wchar_t>> matrix) {
  for (auto const& str : matrix) {
    for (auto const ch : str) {
      std::wcout << ch;
    }
    std::wcout << '\n';
  }
}

int main() {
  std::vector<std::wstring> vec = {L"Cool", L"Cool", L"Cool"};
  print(vec);
}

This doesn't compile. How do I do something like that?

Aykhan Hagverdili
  • 28,141
  • 6
  • 41
  • 93
  • 1
    What's the compilation error? – Gabriel Staples May 08 '21 at 16:41
  • why not a vector of spans? – George May 08 '21 at 16:42
  • 2
    To what array of `std::span` objects are you hoping to refer? It sounds like you want a range of spans, but that (outer) range should perhaps not be a span. – Davis Herring May 08 '21 at 16:44
  • I don't think a span of spans makes any sense. An array or vector of spans does, but not a span of spans. For what a span is, see my answer, or the other answer, here: [What is a "span" and when should I use one?](https://stackoverflow.com/a/61216722/4561887) – Gabriel Staples May 08 '21 at 16:44
  • @GabrielStaples the error is "no conversion from vector to span" – Aykhan Hagverdili May 08 '21 at 16:52
  • @George vector of spans won't work either https://godbolt.org/z/cnjYvjhzW – Aykhan Hagverdili May 08 '21 at 16:55
  • @GabrielStaples vector of spans doesn't compile here either https://godbolt.org/z/cnjYvjhzW – Aykhan Hagverdili May 08 '21 at 16:56
  • @AyxanHaqverdili, responding to the comment under my answer, can you please update your question with calls to `print()` using any input parameters you'd like to see work? Ex: you mentioned `std::wstring` and `std::wstring_view` types should both be valid inputs to `print()`. Anything else? – Gabriel Staples May 08 '21 at 22:31
  • 1
    @Ay: Actually, the error is "no conversion from vector to span". Which is entirely unsurprising. – Deduplicator May 09 '21 at 04:31
  • The problem is that the magic of span – that many containers have contiguous storage – only goes one layer deep: a `std::vector>` is a contiguous collection of `std::vector`s so you can make a `std::span>` from it but not a span of spans. I think there's an ndspan proposal for when the 2D input array is contiguous, but this isn't the case here. I've considered writing a type-erased nested-span type for this case, where the outer container's iteration is done with dynamic lookup. – Ben Jul 17 '21 at 12:51

3 Answers3

5

Why not use a concept instead?

#include <iostream>
#include <string>
#include <vector>
#include <ranges>

template <class R, class T>
concept Matrix = 
    std::convertible_to<
        std::ranges::range_reference_t<std::ranges::range_reference_t<R>>,
        T>;

void print(Matrix<wchar_t> auto const& matrix) {
    for (auto const& str : matrix) {
        for (auto const ch : str) {
            std::wcout << ch;
        }
        std::wcout << '\n';
    }
}

int main() {
  std::vector<std::wstring> vec = {L"Cool", L"Cool", L"Cool"};
  print(vec);
}

godbolt.org

Thanks to Barry for suggesting the simplified concept above using the standard ranges library.

Patrick Roberts
  • 49,224
  • 10
  • 102
  • 153
  • 4
    That concept doesn't check anything. Or rather, it's either `true` or ill-formed... which isn't really what you're looking for in a concept: https://godbolt.org/z/oE9secK48 – Barry May 09 '21 at 00:02
  • @Barry thanks for pointing that out, it took me a while to come up with something that I was satisfied with after you discovered that. The answer above [should work](https://godbolt.org/z/rr4oooxxE). – Patrick Roberts May 09 '21 at 01:53
  • Just use `std::ranges::range`. And then `range_reference_t` is a type trait that gets you the range's reference type. Don't need to roll your own – Barry May 09 '21 at 03:04
  • @Barry, thank you very much for the feedback, I wasn't aware the standard library defined concepts outside of the `` header, I'll update my answer again. – Patrick Roberts May 09 '21 at 03:14
  • 2
    And you can rely on `range_reference_t` already requiring `range`, so just this for short: https://godbolt.org/z/EqWW1n6M1 – Barry May 09 '21 at 03:14
-1

Just use a template to print any container type containing a std::wstring or std::wstring_view (two arbitrary type limitations for the sake of demonstration; easily adjust or remove these limitations as you see fit)

I prefer to stick with code that is more universally-readable (C++ "concepts" are very advanced and not as widely understood). Why not just use this simple template?

template <typename T>
void print(const T& matrix) {
  for (auto const& str : matrix) {
    for (auto const ch : str) {
      std::wcout << ch;
    }
    std::wcout << '\n';
  }
}

As a bonus, add in this static_assert to check types and ensure that only std::wstring or std::wstring_view string types are passed in, for example (modify or delete the static assert as you see fit, and according to your needs):

static_assert(std::is_same_v<decltype(str), const std::wstring&> || 
  std::is_same_v<decltype(str), const std::wstring_view&>,
  "Only strings of `std::wstring` or `std::wstring_view` are "
  "allowed!");

Now you have this better version of the print() function template:

template <typename T>
void print(const T& matrix) {
  for (auto const& str : matrix) {
    static_assert(std::is_same_v<decltype(str), const std::wstring&> || 
      std::is_same_v<decltype(str), const std::wstring_view&>,
      "Only strings of `std::wstring` or `std::wstring_view` are "
      "allowed!");

    for (auto const ch : str) {
      std::wcout << ch;
    }
    std::wcout << '\n';
  }
}

But, the 2nd usage of auto isn't necessary and adds no value (it just obfuscates things), so let's remove it, and use this instead:

for (wchar_t const ch : str) {

The 1st usage of auto is fine because it's required there since it could be multiple types.

(Note: if you actually do need to handle other types of chars here, ignore what I'm saying here and change wchar_t back to auto. That's up to you to decide.)

Now, we have this final version of the printf() function template:

template <typename T>
void print(const T& matrix) {
  for (auto const& str : matrix) {
    static_assert(std::is_same_v<decltype(str), const std::wstring&> || 
      std::is_same_v<decltype(str), const std::wstring_view&>,
      "Only strings of `std::wstring` or `std::wstring_view` are "
      "allowed!");

    for (wchar_t const ch : str) {
      std::wcout << ch;
    }
    std::wcout << '\n';
  }
}

It appears your goal is to be able to print any container type containing wide-char text with your custom print() function, no?

You seem to be calling this a "matrix", where the outer element in the container is a string, and the inner element of each string is a wide character (wchar).

If that's the case, the following template works just fine. I simply changed this:

void print(std::span<std::span<wchar_t>> matrix) {

to this:

template <typename T>
void print(const T& matrix) {

...and then I added:

  1. a static_assert relying on A) std::is_same_v<> (same as std::is_same<>::value) and B) the decltype() specifier to ensure only std::wstring or std::wstring_view string types are passed in, and
  2. some more test prints in main(), including test prints for a std::vector<std::wstring> and a std::vector<std::wstring_view>, as well as for a linked list: std::list<std::wstring_view>, and an unordered set (hash set): std::unordered_set<std::wstring>.

Here is the entire code and print() function template. Run this code online: https://godbolt.org/z/TabW43Yjf.

#include <iostream>
#include <list> // added for demo purposes to print a linked list in main()
// #include <span> // not needed
#include <string>
#include <type_traits> // added to check types and aid with static asserts
#include <unordered_set>
#include <vector>

template <typename T>
void print(const T& matrix) {
  for (auto const& str : matrix) {
    static_assert(std::is_same_v<decltype(str), const std::wstring&> || 
      std::is_same_v<decltype(str), const std::wstring_view&>,
      "Only strings of `std::wstring` or `std::wstring_view` are "
      "allowed!");

    for (wchar_t const ch : str) {
      std::wcout << ch;
    }
    std::wcout << '\n';
  }
}

int main() {
  std::vector<std::wstring> vec1 = {L"Cool1", L"Cool2", L"Cool3"};
  std::vector<std::wstring_view> vec2 = {L"Hey1", L"Hey2", L"Hey3"};
  std::list<std::wstring_view> list1 = {L"You1", L"You2", L"You3"};
  std::unordered_set<std::wstring> set1 = {L"There1", L"There2", L"There3"};
  print(vec1);
  print(vec2);
  print(list1);
  print(set1);

  // Compile-time error due to the std::is_same_v<> usage in the static_assert 
  // above!
  // std::vector<std::string> vec3 = {"hey", "you"};
  // print(vec3);
}

Sample Output:

Cool1
Cool2
Cool3
Hey1
Hey2
Hey3
You1
You2
You3
There3
There2
There1

If you just want to print std::vector<std::wstring> and std::vector<std::wstring_view> types, here is a more-limited template (again, these are two arbitrary type limitations for the sake of demonstration; easily adjust or remove these limitations as you see fit):

Simply replace this in my template above:

template <typename T>
void print(const T& matrix) {

with this, to force it to only accept std::vector<> container types (const T& above changes to const std::vector<T>& below, is all):

template <typename T>
void print(const std::vector<T>& matrix) {

Then, add a static_assert to ensure the type inside the vector is either std::wstring or std::wstring_view, as desired.

Full code below. Run it online here: https://godbolt.org/z/qjhqq647M.

#include <iostream>
// #include <span> // not needed
#include <string>
#include <type_traits>
#include <vector>

template <typename T>
void print(const std::vector<T>& matrix) {
  static_assert(std::is_same_v<T, std::wstring> || 
    std::is_same_v<T, std::wstring_view>,
    "Only vectors of `std::wstring` or `std::wstring_view` are allowed!");

  for (auto const& str : matrix) {
    for (wchar_t const ch : str) {
      std::wcout << ch;
    }
    std::wcout << '\n';
  }
}

int main() {
  std::vector<std::wstring> vec1 = {L"Cool1", L"Cool2", L"Cool3"};
  std::vector<std::wstring_view> vec2 = {L"Hey1", L"Hey2", L"Hey3"};
  print(vec1);
  print(vec2);

  // Compile-time error due to the std::is_same_v<> usage in the static_assert 
  // above!
  // std::vector<std::string> vec3 = {"hey", "you"};
  // print(vec3);
}

Why a span of spans doesn't work:

std::span<T> is essentially just a struct containing a pointer to a block of contiguous memory. Cppreference.com states (emphasis added):

The class template span describes an object that can refer to a contiguous sequence of objects with the first element of the sequence at position zero.

As I explain in my other answer on spans here (What is a "span" and when should I use one?), it might look like this:

template <typename T>
struct span
{
    T * ptr_to_array;   // pointer to a contiguous C-style array of data
                        // (which memory is NOT allocated or deallocated 
                        // by the span)
    std::size_t length; // number of elements in the array

    // Plus a bunch of constructors and convenience accessor methods here
}

Not all C++ container types are stored in contiguous memory, however, such as linked lists (std::list and std::forward_list) so they cannot be placed into a span.

Generally-speaking, a span is a wrapper in C++ to wrap around C-style arrays, capturing in one variable a pointer to their contiguous block of memory, and in another variable, their length. This way, you can replace a function prototype with two input parameters like this:

void do_stuff(T *ptr_to_data, std::size_t num_elements) {}
// OR (the const form)
void do_stuff(const T *ptr_to_data, std::size_t num_elements) {}

with a prototype with one input parameter like this:

void do_stuff(std::span<T> data) {}
// OR (the const form)
void do_stuff(const std::span<T> data) {}

as @mcilloni says in his comment here.

References:

  1. Scratch work:
    1. https://godbolt.org/z/s99dnzj8z
    2. https://godbolt.org/z/33vzTM787
  2. [my answer] What is a "span" and when should I use one?
  3. https://www.learncpp.com/cpp-tutorial/an-introduction-to-stdstring_view/ - EXCELLENT read on what is a std::string_view, when and why to use it, and how. It also covers some of its nuances, limitations, and shortcomings.
  4. https://en.cppreference.com/w/cpp/container/span
  5. https://en.cppreference.com/w/cpp/types/is_same
  6. https://en.cppreference.com/w/cpp/header/type_traits
  7. *****[my answer--VERY USEFUL--I referenced it to remember how to statically check types at compile-time with static_assert(std::is_same_v<decltype(var), some_type>, "some msg");] Use static_assert to check types passed to macro
Gabriel Staples
  • 36,492
  • 15
  • 194
  • 265
  • 1
    @PatrickRoberts, good catch on me missing `const&` in my input parameters to `print()`. That was an oversight of mine for sure. I've fixed it. I've also addressed the other concern about reasonable diagnostic messages, by using a `static_assert` to check the input types passed to the template, and to output a good diagnostic message in case the user uses the wrong type. – Gabriel Staples May 09 '21 at 06:36
  • I appreciate your effort! The problem with your solution is that it's not a reusable concept. You have to scatter these checks all around the code base and keep them in sync. It's also not immediately clear what the input type is unless you go down and read all the asserts within the implementation. Finally, it's limited to to `std::wstring` and `std::wstring_view` and won't handle any other types that contain `wchar_t`. The concepts answer can easily be converted to work with a matrix of any type with minimal effort. – Aykhan Hagverdili May 09 '21 at 09:07
  • @AyxanHaqverdili, `The problem with your solution is that it's not a reusable concept. You have to scatter these checks all around the code base and keep them in sync.` Well, that's not quite true. You don't have to have the checks at all! But, yes, if you want the checks, you have to keep them in-sync. `It's also not immediately clear what the input type is unless you go down and read all the asserts within the implementation.` True, easily solved with a doxygen header for the function. But, I look at the C++ "concept" and I haven't the slightest clue what it is doing or what the type is! – Gabriel Staples May 09 '21 at 13:02
  • @AyxanHaqverdili, `Finally, it's limited to to std::wstring and std::wstring_view & won't handle any other types that contain wchar_t`. That's an arbitrary limitation I forcefully imposed because those are the 2 types you said you wanted in your comment under my downvoted answer. To make it handle more types, simply delete the `static_assert` or add new types to its checks. `The concepts answer can easily be converted to work with a matrix of any type w/minimal effort.` So can my answer. I arbitrarily limited the input types based on your previous comments, trying to guess what you want. – Gabriel Staples May 09 '21 at 13:17
  • @AyxanHaqverdili, Anyway, I think my approach has every bit as much merit, and is easier to read by most C++ developers than the "concepts" approach, which I currently don't understand in the slightest. My goal is to write human-readable code. The fewer the C++ features used, within reason, the better, in my opinion. This allows one to focus on architecture and getting things done rather than creating a never-ending cycle of requiring those who read my code to constantly look up more esoteric C++ syntax in an attempt to understand it. That is my -5 cents. – Gabriel Staples May 09 '21 at 13:17
  • @AyxanHaqverdili, I wish I was smarter, to understand C++ easily. I'm not. I wish I was more knowledgeable to not have to look up C++ syntax constantly. I'm not. I long for the C++ community to value more simplicity. Frequently they don't. I'm trying to change that. – Gabriel Staples May 09 '21 at 13:28
  • 3
    @GabrielStaples: "*The fewer the C++ features used, within reason, the better, in my opinion.*" It's also an excellent way to maintain one's lack of knowledge of these features. You claim that you want C++ to value simplicity, yet the `concept`s feature is *designed* to give you the simplicity you crave. It is meant to allow normal programmers to use complex SFINAE techniques easily. We gave you features to make the language more simple, yet you won't use them. I don't know what it is that you want from the language. – Nicol Bolas May 09 '21 at 17:14
  • @Nicol Bolas, thanks for the response. What I think I want from the language and community is support/permission to use existing techniques, such as I present in this answer above, and acknowledgement and upvotes for presenting them as valid alternatives to the newest features, rather than downvotes and disapproval because they aren't the latest, cutting-edge features. I'll eventually learn the latest features, but one should be allowed to use older techniques to do the same thing as well. – Gabriel Staples May 09 '21 at 17:48
  • What I have done above works, is functionally correct, compiles and has error checking, produces correct output and results, is fairly easy to understand, and is bug free, no? So, why the downvotes? I'd genuinely like to know. – Gabriel Staples May 09 '21 at 17:52
  • 1
    @GabrielStaples: For downvotes... well, by using `std::span` in their hypothetical idea, the OP implicitly asked for a solution that would work for (at least) any contiguous container of contiguous containers of characters. The "error checking" in your solution is specific only to a specific string type (or couple of types). You also suggest that this solution is a priori better than others on the basis of not using C++20 features, even though the question is tagged "C++20". – Nicol Bolas May 09 '21 at 18:32
  • @GabrielStaples: Also, your reason for why `span>` won't work is incorrect. It won't work because, while you may convert any contiguous range of `T` into a span of `T`, you cannot change the type `T` *itself*. Which is what is one is asking for when one wants to turn a container of containers into a span of span. You can create a container of `span`s, but you then would be creating a new container, with its own memory allocation to hold those interior spans. – Nicol Bolas May 09 '21 at 18:35
-6

UPDATE: I'm leaving the answer below despite the downvotes since seeing it and the comments under it has value too, but here's a different answer I just posted instead which I think has value and merit.


I don't understand the desire for using a span at all here (please help me understand if I am missing something), as the purpose of a span is to wrap and "C++-itize" (which is sometimes a debatable practice already) a C-style array.

Why not just change this:

void print(std::span<std::span<wchar_t>> matrix) {

to this?:

void print(std::vector<std::wstring> matrix) {

Now the code works just fine (run on Godbolt):

#include <iostream>
// #include <span> // not needed
#include <string>
#include <vector>

void print(std::vector<std::wstring> matrix) {
  for (auto const& str : matrix) {
    for (auto const ch : str) {
      std::wcout << ch;
    }
    std::wcout << '\n';
  }
}

int main() {
  std::vector<std::wstring> vec = {L"Cool", L"Cool", L"Cool"};
  print(vec);
}

Here is the output, as shown on Godbolt. Notice that the text (Cool Cool Cool) prints just fine:

ASM generation compiler returned: 0
Execution build compiler returned: 0
Program returned: 0
Cool
Cool
Cool
halfer
  • 19,824
  • 17
  • 99
  • 186
Gabriel Staples
  • 36,492
  • 15
  • 194
  • 265
  • The function should be able to handle a vector of `wstring`s and a vector of `wstring_view`s. How does your answer fair in that regard? – Aykhan Hagverdili May 08 '21 at 19:25
  • @AyxanHaqverdili, I didn't realize that was the constraint of the problem, as your question states that nowhere. I've got to run. Will come back to this later. – Gabriel Staples May 08 '21 at 19:27
  • The point of `std::span` is that you can take a container of `T` without caring whether that's a vector, list, deque, std::array, raw-array or something else. It's basically the 2 iterators idiom of STL but packaged in a nicer way. I thought this was obvious. – Aykhan Hagverdili May 08 '21 at 19:33
  • 2
    @AyxanHaqverdili not exactly, a `std::span` is required to refer to a _contiguous_ area of memory, so it is quite limited to only C arrays, `std::array`, `std::vector` and that's it - it is basically a tuple containing a pointer to T and an optional `std::size_t` value containing the number of elements. Its main purpose is to provide an abstraction over the common usage of passing around a pointer and its size, replacing `void do_stuff(T *ptr, std::size_t count)` with `void do_stuff(std::span tees)`. – mcilloni May 08 '21 at 19:58
  • @mcilloni you're right that `std::list` and `std::deque` wouldn't have worked, but the rest would. – Aykhan Hagverdili May 08 '21 at 20:00
  • 1
    @AyxanHaqverdili: It can only do the **contiguous** containers, though, so `std::deque` is out, as is a range formed by converting each element of a container unless you actually form another contiguous container for the results. – Davis Herring May 08 '21 at 20:14
  • @AyxanHaqverdili, here's a template that seems to meet your need, no? It handles any container type containing `std::wstring` or `std::wstring_view` types: https://stackoverflow.com/a/67454463/4561887. – Gabriel Staples May 09 '21 at 05:20