Хорошая практика: когда функция без мутаций должна запрашивать указатель вместо копии? - PullRequest
0 голосов
/ 24 апреля 2019

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

Мне кажется, что копирование такого легкого значения не будет значительным для производительности, и указатель может вызвать больше путаницы, чем простое значение.

Например, рассмотрим следующий код (взят из книги Боба Нистрома "Crafting Interpreters"):

typedef struct {
    TokenType type;
    const char* start;
    int length;
    int line;
} Token;

В следующем фрагменте кода identifiersEqual принимает параметры типа Token* вместо чистогоToken.Это может иметь смысл - нам не нужно копировать Token.

С другой стороны, addLocal принимает просто Token.

С точки зрения общей практики C- Я пытаюсь понять, есть ли конкретная причина, по которой identifiersEqual получает указатель, а addLocal - копию.Обе функции не изменяют значение, и еще раз - Token не имеет большого веса.

Есть ли здесь шаблон, который я пропускаю, или это случайно?В каких случаях мне решать так или иначе?

static bool identifiersEqual(Token* a, Token* b) {
    if (a->length != b->length) return false;
    return memcmp(a->start, b->start, a->length) == 0;
}

static void addLocal(Token name) {
    if (current->localCount == UINT8_COUNT) {
        error("Too many local variables in function.");
        return;
    }

    Local* local = &current->locals[current->localCount++];
    local->name = name;
    local->depth = -1;
}

static void declareVariable() {
    if (current->scopeDepth == 0) return;

    Token* name = &parser.previous;

    for (int i = current->localCount - 1; i >= 0; i--) {
        Local* local = &current->locals[i];
        if (local->depth != -1 && local->depth < current->scopeDepth) break;
        if (identifiersEqual(name, &local->name)) {
            error("Variable with this name already declared in this scope.");
        }
    }

    addLocal(*name);
}

1 Ответ

1 голос
/ 24 апреля 2019

Этот вопрос граничит с вопросом о мнении, поэтому мне, вероятно, следовало бы не говорить, что я, вероятно, не буду использовать эти два интерфейса в одной и той же программе, потому что я стараюсь избегать API, в которых тип данных иногда передается по значению идругие времена по ссылке.Но это только я, поэтому я ограничу остальную часть ответа причиной, по которой вы можете использовать интерфейс, который передает объект средней тяжести по значению.Если вы хотите услышать причины, по которым исходный кодер выбрал этот стиль, вам следует спросить его об этом напрямую.

Первый момент заключается в том, что большинство современных компиляторов смогут избежать копирования, если у них есть доступ к основной части кода.вызываемая функция и сама вызываемая функция достаточно легки, чтобы их можно было встроить.Эти условия, похоже, применяются в случае функций из ссылочного кода, поэтому использование вызова по значению, вероятно, не требует никаких затрат.[Примечание 1] Таким образом, если стиль API предоставляет полезную информацию для программы чтения кода, то ее можно считать полезной.

Теперь рассмотрим прототипы параметров X const * и X.В обоих случаях мы знаем, что переданный аргумент не будет изменен, поэтому нам, конечно, не нужно его копировать.

Но все же может возникнуть обеспокоенность по поводу времени жизни аргумента.Если вызываемая функция берет указатель и сохраняет этот указатель в объекте, который переживет вызов, то нам нужно беспокоиться о принадлежности переданного объекта.По сути, нам нужно было бы передать владение объектом вызываемой функции, а также убедиться, что объект не имеет автоматического времени жизни.В частности, мы не можем вызвать функцию с временным вызовом, и у нас могут возникнуть некоторые сомнения относительно вызова функции с объектом со статическим временем жизни (которое не может быть free d).

С другой сторонывызов по значению явно не накладывает никаких требований на вызывающего.Если вызываемая функция хочет сохранить переданный объект, она отвечает за создание копии и удаление копии, когда она больше не нужна.Мы можем передать ему любой объект, который нам нравится: временный, статический или локальный объект, который будет повторно использован для последующего вызова.

Как это бывает, объекты-токены часто являются локальными объектами, которые повторно используются при разборе.цикл, а не динамически размещаемые объекты, которые накладывают более сложный режим управления памятью.Большую часть времени к объектам токенов, переданным функциям, обращаются только к ним, но иногда их нужно сохранять.Имя функции addLocal довольно убедительно говорит о том, что эта функция сохранит переданный объект.

В этом конкретном случае addLocal фактически сохраняет переданный объект, но сохраняет копию.Это не может быть сделано иначе, потому что передана копия, и копия не переживет вызов.К счастью, оптимизатор почти наверняка встроит addLocal, что позволит избежать ненужной промежуточной копии.Таким образом, использование call-by-value здесь точно сообщило читателю кода, что совершенно не нужно беспокоиться о времени жизни объекта, переданного в addLocal.

В случае identifiersEqualМаловероятно, что вызванная функция должна будет сохранять любой из переданных объектов, поэтому гарантия, возможно, менее важна.Но, как упомянуто выше, я бы, вероятно, написал identifiersEqual в качестве вызова по значению, также для согласованности, надеясь, что компилятору удалось вообще избежать копирования.(Это прыжок веры, и, возможно, мое стремление к последовательности здесь является своего рода тик.)


Примечания

  1. С очень легкими объектами и некоторыми компиляторами возможно, что вызов по значению будет генерировать лучше скомпилированный код.Например, стандартный 64-битный ABI позволяет структурам, которые помещаются в восемь байтов, передаваться в регистрах, что особенно удобно, если объект создается с единственной целью передачи его функции.Я помню, как в те дни, когда я писал в API-интерфейсы OS X GUI, небольшие геометрические объекты всегда передавались маленьким геометрическим объектам по значению и что в руководстве по программированию было примечание, объясняющее, что это было сделано для эффективности.Я не знаю, правда ли это, но я также не думаю, что эта конкретная структура достаточно легкая, чтобы ее можно было применить.Хотя это, возможно, не часто встречается, существуют другие контексты, в которых разъяснение определенным компиляторам, что адрес объекта никогда не берется, позволяет компилятору создавать лучший код.

    В контексте этого вопроса достаточноОбратите внимание, что сгенерированный код для вызова по значению (вероятно) не хуже, чем вызов по ссылке.Если это действительно имеет значение для вас, вам придется проверить сгенерированный код от компилятора, который вам небезразличен.Я этого не делал.

...