ivan100sic's blog

By ivan100sic, history, 16 months ago, In English

As we all know, the C++ standard library class std::bitset is a very powerful tool for solving all kinds of problems [1].

Due to how it is implemented and how C++ works, it is not possible to create a bitset whose size is a run-time variable. However, if the exact size is not a requirement — which is often the case, there is a way to use bitsets that are only slightly larger than required, using template metaprogramming. See the example below. To compile, C++17 or newer is required.

#include <bits/stdc++.h>
using namespace std;

struct Solver {
  int n;

  template<int N>
  void solve() {
    bitset<N> bs;
    std::cout << "Solving with bitset, N=" << N << '\n';
    // do something...
  }

  template<int N>
  void choose_size() {
    if constexpr (N >= 1'000'000) {
      solve<N>();
    } else {
      if (n <= N) {
        return solve<N>();
      }
      choose_size<2*N>();
    }
  }
};

int main() {
  int n;
  cin >> n;

  Solver s{n};
  s.choose_size<32>();
}

If the goal is to optimize further, a tighter constraint can be placed on the relative size of the bitset compared to the argument by modifying the expression in the templated function call choose_size<2*N>(). For example, we can use 5*N/4 to guarantee that the used bitset is at most 25 percent larger than required.

Keep in mind that the number of different sizes used should be a reasonable number. In any case, the compiler must generate a function for each possible template parameter N. If this rule is not respected, it is possible that the compiler will crash or outright refuse to compile the code.

Edit:

Unbeknownst to me, Golovanov399 and clyring have previously written on this exact topic in their comments [2].

Thanks to everyone for sharing additional ideas!

  • Vote: I like it
  • +58
  • Vote: I do not like it

| Write comment?
»
16 months ago, hide # |
 
Vote: I like it +8 Vote: I do not like it
»
16 months ago, hide # |
 
Vote: I like it +11 Vote: I do not like it

solve function can just call itself recursively, which can reduce the boilerplate a bit.

template<int N>
void solve(int n) {
    if (n > N) {
        solve<min(2*N, 1'000'000)>(n);
        return;
    }
    cout << "Solving with bitset, N=" << N << '\n';
}

// call solve<1>(n)
»
16 months ago, hide # |
 
Vote: I like it 0 Vote: I do not like it

Mandatory link to std::tr2::dynamic_bitset blog.

I also have the following function:

    template<int fl = 0>
    void with_bit_floor(size_t n, auto &&callback) {
        if constexpr (fl >= 63) {
            return;
        } else if (n >> (fl + 1)) {
            with_bit_floor<fl + 1>(n, callback);
        } else {
            callback.template operator()<1ULL << fl>();
        }
    }

so that you can pass it a callback lambda: [...]<size_t floor>() {...}, and then use floor as a constexpr variable inside the lambda.