Перемещение семантики и виртуальных методов

В С++ 11 мы в некоторых случаях ориентируемся на передачу объектов по значению, а в других - на const-reference. Однако это руководство зависит от реализации метода, а не только от его интерфейса и предполагаемого использования его клиентами.

Когда я пишу интерфейс, я не знаю, как он будет реализован. Есть ли хорошее правило для написания сигнатур методов? Например, в следующем фрагменте кода следует использовать Bar1 или Bar2?

class IFoo
{
public:
    virtual void Bar1(std::string s) = 0;
    virtual void Bar2(const std::string& s) = 0;
};

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

В следующем примере мы должны передать строку по значению:

class Foo
{
    std::string bar;

    Foo(std::string byValue)
        : bar(std::move(byValue))
    {
    }
};

Теперь мы можем эффективно создавать Foo во всех случаях:

Foo foo1("Hello world"); // create once, move once
Foo foo2(s); // the programmer wants to copy s. One copy and one move
Foo foo3(std::move(t)); // the programmer does not need t anymore. No copy at all

В других случаях мы предпочитаем передавать объекты по ссылке const. Например, в следующем случае мы никогда не хотим копировать/хранить аргумент, просто используйте его методы:

void DoStuff(const std::string& byRef)
{
    std::cout << byRef.length() << std::endl;
}

Все возможные способы использования вышеуказанного метода уже настолько эффективны, насколько это возможно.

Обновление

Я считаю, что забыл показать проблемы с альтернативой const-reference. Если вышеуказанный класс Foo был реализован следующим образом:

class Foo
{
    std::string bar;

    Foo(const std::string& byRef)
        : bar(byRef)
    {
    }
};

Тогда мы получили бы следующие результаты:

Foo foo1("Hello world"); // Here we would have one more copy of the string. It is less efficient.
Foo foo2(s);             // One copy, like before
Foo foo3(std::move(t));  // Irrelevant here.

Алекс.

Ответ 1

Здесь нет "теории всего". У вас все в порядке, есть проблема. Я помню, как он некоторое время назад вернулся.

Мои выводы начались здесь:

Приложение против разработки платформы/библиотеки

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

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

Начальная точка: мы разрабатываем приложения

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

В этот момент мы все еще можем разделить это на 2 случая:

Абстракция к реализации

Есть случаи, когда полезно или даже необходимо определять абстракцию перед реализацией. В таких случаях необходимо осознать, что для определения абстракции требуется гораздо больше исследований по этой проблеме. Например, является ли домен синхронным или асинхронным? Последовательный или параллельный? Высокий или низкий уровень? И другие гораздо более конкретные вопросы.

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

Вывод в этом случае состоит в том, что, если вам действительно нужно определить абстракцию вперед, тогда у вас уже должно быть очень хорошее представление об осуществлении. Лучшая идея, которую вы имеете о ней, тем выше вероятность того, что она действительно станет правильной абстракцией.

Реализация в абстракции

Это мой выбор по умолчанию. Это было сказано многими способами. "Рамки должны быть извлечены", "Извлечь", пока вы не упадете ", и даже" Конвенция по конфигурации "имеет некоторое сходство в концепции.

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

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

Важно не попасться и все еще держать ноги на земле. Я видел, что многие попытки абстракции не совпадают с точкой, где нет никакого способа рассуждать о ее имени и выводить его намерение, кроме чтения тысяч строк кода, которые его используют. Например, в текущей базе кода, над которой я работаю, тип, который должен был называться Image, называется BinaryData. Во всем коде есть попытки рассматривать его как конкретное (изображение), и как абстрактное понятие в то же время.

Подводя итог

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

Ответ 2

Вы также можете указать перегрузку для Bar2, которая принимает ссылку rvalue:

class IFoo
{
public:
    virtual void Bar2(const std::string& s) = 0;

    virtual void Bar2(std::string&& s)
    {
        Bar2(s);   // calls the const& overload because s is an lvalue
    }
};

По умолчанию опорная перегрузка rvalue просто вызывает опорную ссылку const lvalue. Но если конкретный подкласс может использовать ссылки rvalue, избыточная эталонная перегрузка rvalue может быть переопределена.

Ответ 3

Я считаю, что он должен определенно зависеть от реализации. Как вытекает из вашего вопроса, запрещая полностью "всегда лучшую" подпись, единственная разумная вещь - выбрать подпись таким образом, чтобы оптимизировать текущую реализацию. Если вы напишете интерфейс перед кодом - возьмите обоснованное предположение и попытайтесь маневрировать себе таким образом, чтобы вы могли дождаться первой реализации, прежде чем совершать подпись.

Оперативные слова здесь являются "первыми" и "текущими". Что произойдет, если вы ошибаетесь? Что произойдет, если на каком-то более позднем этапе подпись не позволит вашему коду быть оптимальным? Вот что вы можете сделать:

Нет обязательств

Если это достаточно скоро - просто измените его. Из определения понятия "нет обязательств", правильно?

Выполнено для API

Для конкретного примера предположим, что вы выбрали неверное, и пошли с этим:

virtual void DoStuff(std::string s) = 0;

Однако, как оказалось, копирование не требуется (так же, как и исходная реализация DoStuff). Вот что вы можете сделать:

// stuff.h
virtual void DoStuff_Optimized(const std::string & s);
virtual void DoStuff(std::string s);

// stuff.cc
virtual void DoStuff_Optimized(const std::string & s);
{
    // Fast implementation of DoStuff, no copying necessary
    std::cout << s.length() << std::endl;
}

virtual void DoStuff(std::string s)
{
    DoStuff_Optimized(s);
}

Существующие клиенты получат более низкую производительность. Новые клиенты могут использовать версию Optimized.

Выполнено для ABI

На данный момент, к сожалению, вы ничего не можете сделать. Однако, если вы осторожны, вы можете следить за действиями "Committed to API". (В частности, мой пример не сохранит совместимость с ABI).