2

In C++ suppose one has an integer one wants to check if it belongs to a given ad hoc set, the equivalent of the python:

if i in [4, 6, 7]: ...

Surely there must be a more concise and legible way than defining a std::vector in one line and then using the lengthily std::find in another, right?

nbubis
  • 2,304
  • 5
  • 31
  • 46
  • What about https://en.cppreference.com/w/cpp/algorithm/ranges/contains or https://en.cppreference.com/w/cpp/algorithm/all_any_none_of or https://en.cppreference.com/w/cpp/container/set/count ? – Jesper Juhl Apr 25 '23 at 11:08

2 Answers2

4

You could create a helper function:

template <class T, class... Args>
bool is_in(const T& v, const Args&... args) {
    return (... || (v == args));         // fold expression to check
}

int main(int argc, char**) {
    return is_in(argc, 4, 6, 7);
}

The above program compiles into something like this with g++13.1:

main:
        lea     eax, [rdi-6]
        cmp     eax, 1
        setbe   al
        cmp     edi, 4
        sete    dl
        or      eax, edx
        movzx   eax, al
        ret

A version without writing a separate function template could use a lambda to do the same thing ...

int main(int argc, char**) {
    return [&](auto&&... args){ return (... || (argc == args)); }(4, 6, 7);
}

... and it compiles into exactly the same thing as the function template instantiation with g++.


If you, for example, populate a std::set in order to do the same check you miss out on the short-circuit evaluation that the fold expression helps with. You also need to instantiate and populate the std::set before making the query. This comes with some complexity that the compiler might not be able to optimize away.

Using a std::set for the same query ...

#include <set>

int main(int argc, char**) {
    return std::set{4, 6, 7}.contains(argc);
}

... and it compiles into something like this with the highest optimization enabled:

main:
        push    r15
        xor     r15d, r15d
        push    r14
        mov     r14d, edi
        push    r13
        push    r12
        push    rbp
        push    rbx
        sub     rsp, 72
        mov     rax, QWORD PTR .LC0[rip]
        lea     r13, [rsp+24]
        mov     rbp, rsp
        mov     DWORD PTR [rsp+8], 7
        mov     QWORD PTR [rsp], rax
        mov     DWORD PTR [rsp+24], 0
        mov     QWORD PTR [rsp+32], 0
        mov     QWORD PTR [rsp+40], r13
        mov     QWORD PTR [rsp+48], r13
        mov     QWORD PTR [rsp+56], 0
.L67:
        mov     r12d, DWORD PTR [rbp+0]
        test    r15, r15
        je      .L59
        mov     rbx, QWORD PTR [rsp+48]
        cmp     r12d, DWORD PTR [rbx+32]
        jle     .L59
.L60:
        mov     r15d, 1
        cmp     rbx, r13
        jne     .L92
.L66:
        mov     edi, 40
        call    operator new(unsigned long)
        mov     DWORD PTR [rax+32], r12d
        mov     rsi, rax
        movzx   edi, r15b
        mov     rcx, r13
        mov     rdx, rbx
        call    std::_Rb_tree_insert_and_rebalance(bool, std::_Rb_tree_node_base*, std::_Rb_tree_node_base*, std::_Rb_tree_node_base&)
        mov     rax, QWORD PTR [rsp+56]
        lea     r15, [rax+1]
        mov     QWORD PTR [rsp+56], r15
.L65:
        add     rbp, 4
        lea     rax, [rsp+12]
        cmp     rbp, rax
        jne     .L67
        mov     rdi, QWORD PTR [rsp+32]
        mov     rbx, r13
        mov     rax, rdi
        test    rdi, rdi
        jne     .L68
        jmp     .L69
.L94:
        mov     rax, rcx
        test    rax, rax
        je      .L93
.L68:
        mov     rdx, QWORD PTR [rax+16]
        mov     rcx, QWORD PTR [rax+24]
        cmp     DWORD PTR [rax+32], r14d
        jl      .L94
        mov     rbx, rax
        mov     rax, rdx
        test    rax, rax
        jne     .L68
.L93:
        cmp     rbx, r13
        je      .L69
        cmp     DWORD PTR [rbx+32], r14d
        jle     .L69
        mov     rbx, r13
.L69:
        call    std::_Rb_tree<int, int, std::_Identity<int>, std::less<int>, std::allocator<int> >::_M_erase(std::_Rb_tree_node<int>*) [clone .isra.0]
        xor     eax, eax
        cmp     rbx, r13
        setne   al
        add     rsp, 72
        pop     rbx
        pop     rbp
        pop     r12
        pop     r13
        pop     r14
        pop     r15
        ret
.L59:
        mov     rbx, QWORD PTR [rsp+32]
        test    rbx, rbx
        jne     .L63
        jmp     .L95
.L74:
        mov     rbx, rax
.L63:
        cmp     r12d, DWORD PTR [rbx+32]
        mov     rax, QWORD PTR [rbx+24]
        cmovl   rax, QWORD PTR [rbx+16]
        setl    dl
        test    rax, rax
        jne     .L74
        mov     rax, rbx
        test    dl, dl
        jne     .L96
.L64:
        cmp     r12d, DWORD PTR [rax+32]
        jle     .L65
        jmp     .L60
.L92:
        cmp     r12d, DWORD PTR [rbx+32]
        setl    r15b
        jmp     .L66
.L96:
        cmp     rbx, QWORD PTR [rsp+40]
        je      .L60
.L72:
        mov     rdi, rbx
        call    std::_Rb_tree_decrement(std::_Rb_tree_node_base*)
        jmp     .L64
.L95:
        mov     rbx, r13
        cmp     QWORD PTR [rsp+40], r13
        jne     .L72
        mov     r15d, 1
        jmp     .L66
        mov     rbx, rax
        jmp     .L70
main.cold:
.LC0:
        .long   4
        .long   6

Notice the calls like call std::_Rb_tree_insert_and_rebalanc which aren't necessarily cheap

For the sake of readability, don't use the lambda or the std::set way.

For the sake of efficiency, don't use a std::set in these situations.

Write a small function (or function template) with a good name that makes it clear what you're doing, or, if you only have three values to check, do the check explicitly:

if(i == 4 || i == 6 || i == 7) {
} else {
}
switch(i) {
case 4:
case 6:
case 7:
    // do stuff
    break;
default: ... // the else case
}
Ted Lyngmo
  • 93,841
  • 5
  • 60
  • 108
1

Most concise way found so far is to define a std::set, and check if the element is inside it using the count method, i.e.:

if (std::set <int> {4, 6, 7}.count(i) > 0) { ...
nbubis
  • 2,304
  • 5
  • 31
  • 46