Добавить класс друзей после объявления

Я пытаюсь написать сервер именованных каналов на С++. У меня есть класс с именем client_pool, который содержит контейнер экземпляров канала и одну общедоступную функцию-член write, которая асинхронно отправляет данные всем подключенным клиентам.

Проблема в том, что клиенты имеют тенденцию неожиданно отключаться. Когда это происходит, вызов WriteFileEx завершается с ошибкой ERROR_NO_DATA. Когда это произойдет, я хочу перейти к классу client_pool и сказать ему закрыть дескриптор клиента и удалить его из контейнера. Однако, поскольку WriteFileEx чертовски сложно использовать, я создал вспомогательный класс с именем write_context в анонимном пространстве имен.

Таким образом, конечным результатом является то, что я хочу вызвать закрытый метод в client_pool, который объявлен в clients.h, из класса write_context, который объявлен в clients.cpp. Что-то вроде этого (детали/обработка ошибок опущены):

clients.h

class client_pool {
    struct implementation;
    std::unique_ptr<implementation> pimpl;
public:
    void write(uint8_t *data, size_t size);
};

clients.cpp

struct client_pool::implementation {
    set<HANDLE> connected;
    // ...
    void disconnect(HANDLE victim)
    {
        CloseHandle(victim);
        connected.erase(victim);
    }
};

namespace { struct write_context {
    OVERLAPPED overlapped;
    client_pool *owner;
    HANDLE target;
    const uint8_t *buffer;
    size_t total_size;
    size_t written;
    // ...
    void next_chunk()
    {
        if(!WriteFileEx(/* ... */, write_context::completion_routine)) {
            if(GetLastError() == ERROR_NO_DATA) {
                // I want to do something like
                owner->pimpl->disconnect(target);
            }
        }
    }
    static void CALLBACK completion_routine(DWORD errcode, DWORD transferred, LPOVERLAPPED overlapped)
    {
        auto self = reinterpret_cast<write_context*>(overlapped);
        self->written += transferred;
        if(errcode == ERROR_MORE_DATA) {
            self->next_chunk();
        } else {
            delete self;
        }
    }
}; }

void client_pool::write(uint8_t *data, size_t size)
{
    for each handle in pimpl->connected {
        auto context = new write_context(this, handle, data, size);
        context->next_chunk();
    }
}

Очевидно, что строка owner->pimpl->disconnect(target); не компилируется, потому что pimpl является приватной. Что я могу сделать / каковы мои альтернативы?


person Raphael R.    schedule 13.11.2011    source источник


Ответы (4)


Прямой доступ к pimpl->connected и write_context непосредственно в вашем методе client_pool::write несколько противоречит смыслу идиомы pimpl. Вы, вероятно, могли бы сделать это иначе, пока не столкнетесь с такой же проблемой.

Я бы просто создал метод реализации::write, для которого вы можете передать аргументы и указатель на client_pool.

person Gerald    schedule 13.11.2011

Я думаю, что если вы используете именованное пространство имен вместо анонимного пространства имен, вы можете поместить эту строку в определение класса:

friend void namespace_name::next_chunk()

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

Или есть жестокие и ужасающие:

#define class struct
#define private public
#define protected public
person Joshua    schedule 13.11.2011
comment
Не такое чистое, как решение Джеральда, но достаточно хакерское, чтобы проголосовать за него. - person Raphael R.; 14.11.2011

Сделать write_context другом implementation. Передайте pimpl как owner из write_context.

person rob mayoff    schedule 13.11.2011

Извините, но это не лучший способ использовать идиому PIMPL.

PIMPL скрывает детали реализации своего владельца, и доступ к нему должен осуществляться только через интерфейс его владельцев. Итак, если вы хотите вызвать метод «client_pool::implementation», его следует переместить в интерфейс «client_pool», а его реализация должна делегировать работу классу «client_pool::implementation». В другом случае это выглядит как ошибка дизайна.

person vladv    schedule 13.11.2011
comment
Проблема в том, что метод disconnect() был бы частью общедоступного интерфейса, хотя на самом деле это не так. Метод write() — это единственный метод, который нужен интерфейсу. Цель разъединения () состоит в том, чтобы избавиться от мертвых каналов - на мой взгляд, деталь реализации. - person Raphael R.; 13.11.2011
comment
В вашей архитектуре write_context::next_chunk выполняет всю работу по записи и обнаруживает, что канал мертв. Похоже, ему нужен только дескриптор канала, и он может работать отдельно от client_pool. Вы можете вернуть соответствующее возвращаемое значение или выдать исключение в этом методе, чтобы сигнализировать о том, что канал мертв, и обработать такую ​​ситуацию в client_pool::write, отключив мертвый канал. С помощью этого рефакторинга вы можете устранить ненужные зависимости между классами: классу write_context не потребуется client_pool для выполнения своей работы. - person vladv; 13.11.2011
comment
Нет, я не могу. next_chunk вызывается асинхронно. - person Raphael R.; 14.11.2011