Какой хороший шаблон для вычисления переменной только при первом использовании?

Не актуальная проблема, но я ищу шаблон для улучшения следующей логики:

void PrintToGameMasters()
{
    std::string message = GetComplicatedDebugMessage(); // This will create a big string with various info
    for (Player* player : GetAllPlayers())
       if (player->IsGameMaster())
           player->SendMessage(message);
}

Этот код работает, но у меня есть проблема в том, что в большинстве случаев нет игроков gamemasters, поэтому составление сообщения будет сделано даром.

Я хотел бы написать что-то, что могло бы создать сообщение только при первом использовании этой переменной, но я не могу найти здесь хорошего решения.

ОБНОВЛЕНИЕ: Чтобы сделать этот вопрос более точным, я ищу решение, которое не относится к строкам, это может быть тип без функции, чтобы проверить, инициализируется ли она. Также большие бонусные баллы, если мы сможем удержать вызов GetComplicatedDebugMessage на вершине цикла, я думаю, что решение, включающее обертку, решит эту проблему.

Ответы

Ответ 1

Принимая во внимание, что std::string имеет пустое значение, которое может означать "не вычисляется", вы можете использовать более широко std::optional, который обрабатывает пустую строку и типы, которые нельзя создать по умолчанию:

void PrintToGameMasters()
{
    std::optional<std::string> message;

    for (Player* player : GetAllPlayers()) {
       if (player->IsGameMaster()) {
           if (!message) {
              message = GetComplicatedDebugMessage();
           }
           player->SendMessage(*message);
       }
    }
}

Ответ 2

Используйте ориентированный на данные дизайн; храните два списка игроков: мастеров игр и неигровых мастеров (или всех игроков, которые есть у вас сейчас + отдельный вектор указателей только для мастеров игр).

void PrintToGameMasters()
{
    auto players = GetGameMasters(); // Returns ONLY game master players
    if (players.begin() != players.end()) {
        std::string message = GetComplicatedDebugMessage();
        for (Player* player : players) {
            player->SendMessage(message);
        }
    }
}

Цель - минимизировать if -statements внутри петель.

Оптимизируйте для наиболее распространенного случая, а не для наиболее общего; наиболее распространенным случаем является то, что игрок не является мастером игры; так что избегайте их зацикливания.


Постскриптум Поскольку вы разрабатываете игру, я хочу добавить эту ссылку в Mike Acton cppcon talk, которая может вас заинтересовать.

Ответ 3

Здесь есть несколько хороших идей, но мне бы хотелось, чтобы это было немного проще:

void PrintToGameMasters()
{
    std::string message;

    for (Player* player : GetAllPlayers())
    {
       if (player->IsGameMaster())
       {
           if (message.empty())
              message = GetComplicatedDebugMessage();

           player->SendMessage(message);
       }
    }
}

Каждый может следить за этим, и это дешево, как фишки & hellip; плюс это легко, как пирог для отладки.

Ответ 4

Вы можете использовать std::call_once с лямбдой для вызова функции при первом обнаружении мастера игры, такого как

void PrintToGameMasters()
{
    std::once_flag of;
    std::string message;
    for (Player* player : GetAllPlayers())
       if (player->IsGameMaster())
       {
           std::call_once(of, [&](){ message = GetComplicatedDebugMessage(); });
           player->SendMessage(message);
       }
}

Ответ 5

Оберните сообщение в изменчивую лямбду:

auto makeMessage = [message = std::string()]() mutable -> std::string&
{
    if (message.empty()) {
        message = GetComplicatedDebugMessage();
    }
    return message;
};

for (Player* player : GetAllPlayers())
   if (player->IsGameMaster())
       player->SendMessage(makeMessage());

Ответ 6

Не уверен, что это лучший шаблон, но вы можете отложить вычисление с помощью лямбды:

void PrintToGameMasters()
{
    std::string message = "";
    auto getDebugMessage = [&message]() -> const std::string& { 
        if (message.empty()) {
            message = GetComplicatedDebugMessage();
        }
        return message;
    };

    for (Player* player : GetAllPlayers())
       if (player->IsGameMaster())
           player->SendMessage(getDebugMessage());
}

Ответ 7

Вы можете расширить подход, используя std::optional (как в ответе Jarod41) с ленивой оценкой сверху. Это также удовлетворяет требованию "держать вызов GetComplicatedDebugMessage на вершине цикла".

template <typename T>
class Lazy : public std::optional<T> {
public:
    Lazy(std::function<T()> f) : fn(f) { }
    T operator*() {
        if (!*this)
            std::optional<T>::operator=(fn());
        return this->value();
    }
private:
    std::function<T()> fn;
};

void PrintToGameMasters()
{
    Lazy<std::string> message(GetComplicatedDebugMessage);
    for (Player* player : GetAllPlayers())
        if (player->IsGameMaster())
            player->SendMessage(*message);
}

Ответ 8

Я не уверен, почему вы хотите оставить определение message над циклом. Если кто-то читает код, чтобы проанализировать, что происходит в случае, когда std::end(GetAllPlayers())==std::begin(GetAllPlayers)(), вы не хотите загромождать свое умственное рабочее пространство нерелевантными переменными.

Если вы готовы отказаться от этого, то static ваш друг:

void PrintToGameMasters()
{
    for (auto const &player : GetAllPlayers())
        if (player->IsGameMaster())
        {
            //Initialization of a static variable occurs exactly once, even when multithreaded,
            //precisely when the defining line is hit for the first time
            static auto const &message{GetComplicatedDebugMessage()};
            player->SendMessage(message);
        }
}

Ответ 9

Это работает. Как будет сказано в лицензии MIT:

ПРОГРАММНОЕ ОБЕСПЕЧЕНИЕ ПРЕДОСТАВЛЯЕТСЯ "КАК ЕСТЬ", БЕЗ КАКИХ-ЛИБО ГАРАНТИЙ, ЯВНЫЕ ИЛИ ПОДРАЗУМЕВАЕМЫЕ

#include <Windows.h>
#include <cstdlib>
#include <cstdio>
#include <string>

struct Player {
    bool isGameMaster;
    int id;
};

int __stdcall IsGameMaster(Player* self) {
    return self->isGameMaster ? 1 : 0;
}

// Could've been "SendMessage"... but Windows.h
void __stdcall SendMessageToPlayer(Player* self, std::string* msg) {
    printf("Player %d says: %s\n", self->id - 1000 + 1, msg->c_str());
}

Player g_players[18];
Player* __stdcall GetAllPlayers(void){
    return &g_players[0];
}

std::string le_message = "hi, I'm a game master";
std::string* __stdcall GetComplicatedMessage(void) {
    puts("GENERATING COMPLICATED MESSAGE. HOGGING CPU FOR 3 DAYS!");
    return &le_message; // to make my assembly life easier
}

__declspec(naked) void PrintToGameMasters(void){
    __asm {
        push ebp;
        mov ebp, esp;
        sub esp, 8;

        call GetAllPlayers;
        mov [ebp-4], eax;

        // this is 'i', the loop iteration counter
        // I chose esi because it is preserved by stdcalls
        xor esi, esi;

        do_loop:

        // Player* player = &g_players[i];
        mov ebx, esi;
        imul ebx, SIZE Player;
        add ebx, [ebp-4]; // ebx = g_players + sizeof(Player) * i, or &g_players[i]

        // if (player->IsGameMaster()) {
        push ebx;
        call IsGameMaster;
        test eax, eax;
        jz no_print;

        // msg = GetComplicatedMessage();
        get_msg_start:
        call GetComplicatedMessage;
        mov [ebp-8], eax;
        jmp short delete_self;
        get_msg_end:

        // player->SendMessage(msg);
        push [ebp-8];
        push ebx;
        call SendMessageToPlayer;

        // }
        no_print:
        inc esi;
        cmp esi, 18;
        jb do_loop;

        mov esp, ebp;
        pop ebp;
        ret;

        delete_self:
        mov ecx, get_msg_start;
        mov eax, get_msg_end;

        sub eax, ecx;
        mov byte ptr [ecx], 0xEB; // jmp short
        mov byte ptr [ecx+1], al; // relative offset
        jmp get_msg_end;
    }
}

int main(){
    for (int i = 0; i < 18; i++) {
        g_players[i].isGameMaster = (i == 12 || i == 15); // players 13 and 16
        g_players[i].id = i + 1000;
    }

    DWORD oldProtect;
    VirtualProtect(&PrintToGameMasters, 0x1000, PAGE_EXECUTE_READWRITE, &oldProtect);

    PrintToGameMasters();

    return 0;
}

code working as intended

Это намного быстрее, чем подход if (!message) message = GetMessage(), если только у вас нет ЦП с предиктором ветвления (что вы, скорее всего, делаете). В этом случае он медленнее (или, возможно, одинаково быстро, но не быстрее), уродливее, менее переносим и, скорее всего, убьет вас психопатом.

Ответ 10

Вы можете использовать пользовательский локальный тип с оператором преобразования:

void PrintToGameMasters()
{
    struct {
        operator std::string const &(void)
        {
            static auto const real_value{GetComplicatedDebugMessage()};
            return real_value;
        }
    } message;
    for (auto const &player : GetAllPlayers())
       if (player->IsGameMaster())
           player->SendMessage(message);
}

Конечно, это предполагает, что GetComplicatedDebugMessage фактически не имеет состояния. В противном случае вам придется прибегнуть к захвату лямбды или аналогичным трюкам, описанным в других ответах здесь.

Ответ 11

Это буквально одна из вещей, которые std::future предназначена для решения:

void PrintToGameMasters()
{
    auto message = std::async(
        std::launch::deferred,
        []{return GetComplicatedDebugMessage();}
    );
    for (Player* player : GetAllPlayers())
       if (player->IsGameMaster())
           player->SendMessage(message.get());
}

Вызов std::async с помощью std::launch::deferred заставляет задачу "выполняться в вызывающем потоке при первом запросе ее результата".

Ответ 12

Очень надеюсь, что это поможет

Попробуйте, возможно, реализовать эту логику:

#include <iostream>

using namespace std;

int main()
{
    bool GameMaster,first_time,used_before;

    GameMaster = true;
    first_time = false;
    used_before = false;

    GameMaster = first_time;
    first_time = used_before;


    for( int i = 0; i < 5; i++ ) {

        if(GameMaster==used_before) {
            cout<<"    First Time";
            GameMaster = true;
        }

        if(GameMaster!=used_before) {
            cout<<"    Old News";
        }
    }

    return 0;
}

с ответом:

  First Time    Old News    Old News    Old News    Old News    Old News 

Ответ 13

static переменные инициализируются впервые. Итак:

void PrintToGameMasters()
{
    for (Player* player : GetAllPlayers())
       if (player->IsGameMaster()) {
           static std::string message = GetComplicatedDebugMessage(); // This will create a big string with various info
           player->SendMessage(message);
       }
}

Это, конечно, если предположить, что вычисление значения не чаще, чем один раз за вызов, является правильным способом продолжения. Из того, как вы сформулировали свою задачу, не ясно, так ли это.