Generating random permutation with forced change of order (Secret Santa)
I would suggest breaking the actual permutation algorithm out into its own function, since it’s useful on its own. That would pretty much solve the readability problem.
And then I would suggest doing the permutation with indices, rather than copying the whole input vector. (Indices would also work for other range types, especially if you limit the input range type to random-access, which is not unreasonable for a sorting algorithm.) Once you’re dealing with indices, detecting positions that are unchanged is trivial, and quick:
template &lt;std::sized_range Rng, typename Gen&gt;
requires std::uniform_random_bit_generator&lt;std::remove_reference_t&lt;Gen&gt;&gt;
auto get_fully_permuted_indices(Rng const&amp; rng, Gen&amp;&amp; gen)
{
    auto idxs = std::vector&lt;std::size_t&gt;(std::ranges::size(rng));
    std::iota(idxs.begin(), idxs.end(), std::size_t(0));

    auto permuted = false;
    do
    {
        std::ranges::shuffle(idxs, gen);

        permuted = true; // hope for the best!
        for (auto i = std::size_t(0); i &lt; res.size(); ++i)
        {
            // if the value at i is the same as i, then this is not properly permuted
            if (i == idxs[i])
            {
                permuted = false;
                break;
            }
        }
    } while (not permuted);

    return idxs;
}

Once you have that, your main function is trivial:
std::map&lt;std::string, std::string&gt; getPermutation(const std::vector&lt;std::string&gt;&amp; participants)
{
    auto const idxs = get_fully_permuted_indices(participants, std::mt19937{std::random_device(){}});

    auto res = std::map&lt;std::string, std::string&gt;{};
    for (auto i = std::size_t(0); i &lt; std::ranges::size(participants); ++i)
        res.emplace(participants[i], participants[idxs[i]]);

    return res;
}

I would also suggest moving the creation of the RNG out of the function, and passing the RNG in as an argument:
template &lt;typename Gen&gt;
requires std::uniform_random_bit_generator&lt;std::remove_reference_t&lt;Gen&gt;&gt;
auto getPermutation(const std::vector&lt;std::string&gt;&amp; participants, Gen&amp;&amp; gen)
{
    auto const idxs = get_fully_permuted_indices(participants, std::forward&lt;Gen&gt;(gen));

    auto res = std::map&lt;std::string, std::string&gt;{};
    for (auto i = std::size_t(0); i &lt; std::ranges::size(participants); ++i)
        res.emplace(participants[i], participants[idxs[i]]);

    return res;
}

This goes with the doctrine of “one function, one job”, and allows both reusing the RNG, and testing the function with a known RNG.
If you want to go even further, an “is-fully-permuted-indices” algorithm, to remove that for loop in get_fully_permuted_indices(), also seems useful:
template &lt;std::input_range Rng&gt;
requires std::equality_comparable_with&lt;std::ranges::range_value_t&lt;Rng&gt;, std::size_t&gt;
constexpr auto is_fully_permuted_indices(Rng const&amp; rng)
{
    auto idx = std::size_t(0);
    for (auto&amp;&amp; item : rng)
    {
        if (item == idx++)
            return false;
    }

    return true;
}

template &lt;std::sized_range Rng, typename Gen&gt;
requires std::uniform_random_bit_generator&lt;std::remove_reference_t&lt;Gen&gt;&gt;
auto get_fully_permuted_indices(Rng const&amp; rng, Gen&amp;&amp; gen)
{
    auto idxs = std::vector&lt;std::size_t&gt;(std::ranges::size(rng));
    std::iota(idxs.begin(), idxs.end(), std::size_t(0));

    do
    {
        std::ranges::shuffle(idxs, gen);
    } while (not is_fully_permuted_indices(idxs));

    return idxs;
}