C++
combinations
vectors
programming
algorithms

Howto create combinations of several vectors without hardcoding loops in C?

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

If the number of input vectors can change, hardcoded nested loops are the wrong tool. The generic problem is the cartesian product of several collections: pick one element from each vector and emit every possible result.

In C++, the cleanest solution is usually recursion or backtracking over a std::vector<std::vector<T>>. That way the algorithm depends on the data shape, not on how many for loops you wrote by hand.

Model the Input as a Vector of Vectors

Instead of naming each vector separately, store them in one outer container:

cpp
1#include <iostream>
2#include <vector>
3
4int main() {
5    std::vector<std::vector<int>> groups = {
6        {1, 2},
7        {10, 20, 30},
8        {100, 200}
9    };
10
11    std::cout << groups.size() << '\n';
12}

Now the code does not care whether there are three groups or thirty. That is the key design shift.

Generate the Cartesian Product Recursively

The usual pattern is to keep a partially built combination and extend it one group at a time:

cpp
1#include <iostream>
2#include <vector>
3
4void buildCombinations(
5    const std::vector<std::vector<int>>& groups,
6    std::size_t depth,
7    std::vector<int>& current
8) {
9    if (depth == groups.size()) {
10        for (int value : current) {
11            std::cout << value << ' ';
12        }
13        std::cout << '\n';
14        return;
15    }
16
17    for (int value : groups[depth]) {
18        current.push_back(value);
19        buildCombinations(groups, depth + 1, current);
20        current.pop_back();
21    }
22}
23
24int main() {
25    std::vector<std::vector<int>> groups = {
26        {1, 2},
27        {10, 20, 30},
28        {100, 200}
29    };
30
31    std::vector<int> current;
32    buildCombinations(groups, 0, current);
33}

This prints every possible combination while using only one recursive function.

Make It Reusable with a Callback

Real programs rarely want to print combinations directly. A callback makes the generator reusable:

cpp
1#include <functional>
2#include <vector>
3
4void buildCombinations(
5    const std::vector<std::vector<int>>& groups,
6    std::size_t depth,
7    std::vector<int>& current,
8    const std::function<void(const std::vector<int>&)>& visit
9) {
10    if (depth == groups.size()) {
11        visit(current);
12        return;
13    }
14
15    for (int value : groups[depth]) {
16        current.push_back(value);
17        buildCombinations(groups, depth + 1, current, visit);
18        current.pop_back();
19    }
20}

Now the caller decides whether to print, store, or score each combination.

Think About Empty Inputs

You should decide up front how the algorithm behaves in edge cases:

  • if one inner vector is empty, there are no combinations
  • if the outer collection is empty, some codebases treat that as one empty combination

The exact convention depends on your application, but it should be explicit rather than accidental.

For example, a quick guard for empty inner groups can prevent wasted work:

cpp
1bool hasEmptyGroup(const std::vector<std::vector<int>>& groups) {
2    for (const auto& group : groups) {
3        if (group.empty()) {
4            return true;
5        }
6    }
7    return false;
8}

Remember the Size Can Explode

Generic code removes the hardcoded loops, but it does not remove combinatorial growth. If you have five vectors of size ten, the result count is already one hundred thousand. At ten vectors of size ten, the output becomes enormous.

That means the real performance strategy is often:

  • stream combinations one at a time
  • stop early when possible
  • avoid storing every result unless you genuinely need all of them

The callback form helps with this because it lets you process each combination as it is produced.

Common Pitfalls

  • Writing nested loops tied to the current number of vectors.
  • Forgetting to represent the input as a single collection of collections.
  • Storing every combination in memory when a streaming visitor would be enough.
  • Ignoring empty-vector edge cases.
  • Assuming a generic implementation changes the underlying combinatorial cost.

Summary

  • Treat the problem as a cartesian product over a std::vector<std::vector<T>>.
  • A recursive backtracking function is the simplest generic solution in C++.
  • Use a callback when the caller needs to process combinations instead of printing them.
  • Handle empty inputs deliberately.
  • Keep in mind that the number of combinations can grow very quickly even with clean code.

Course illustration
Course illustration

All Rights Reserved.