Pull to refresh

Comments 134

Еще есть очень полезная штука как placament new:
void* operator new (std::size_t size, void* ptr) noexcept;
Который размещает данный объект в память указанную в «ptr» и вызывает его конструктор.
Достаточная простая и удобная оптимизация или просто стратегия хранения сложных объектов которые нуждаются в вызове конструкторов.

Я бы не рекомендовал использовать placement new без крайней необходимости (реализация GC, пулов памяти и прочих "хакерских штучек"): затуманивает смысл кода и представляет собой отличный экземпляр грабель с концом ручки как раз на уровне паха, т.к. деструктор придётся вызывать вручную, а это легко забыть сделать. К тому же, нужно самому следить, чтобы размер памяти был достаточен для размещения объекта, а не то конструктор инициализирует соседний кусок памяти.

деструктор придётся вызывать вручную, а это легко забыть сделать
Но зачем? Можно просто подсунуть unique_ptr правильный deleter:
#include <memory>
#include <type_traits>
#include <iostream>

class MyClass
{
public:
	MyClass() { std::cout << "constructor\n"; }
	~MyClass() { std::cout << "destructor\n"; }
	void DoSomething() { std::cout << "doing something\n"; }
};

struct DeleterCallingDestructor
{
	template<typename T>
	void operator()(T* ptr) const { ptr->T::~T(); }
};

int main()
{
	std::aligned_storage<sizeof(MyClass), alignof(MyClass)> storage;
	std::unique_ptr<MyClass, DeleterCallingDestructor> ptr(new MyClass);
	ptr->DoSomething();
	return 0;
}
Тьфу, сконцентировался на deleter и забыл собственно placement new:
#include <memory>
#include <type_traits>
#include <iostream>

class MyClass
{
public:
	MyClass() { std::cout << "constructor\n"; }
	~MyClass() { std::cout << "destructor\n"; }
	void DoSomething() { std::cout << "doing something\n"; }
};

struct DeleterCallingDestructor
{
	template<typename T>
	void operator()(T* ptr) const { ptr->T::~T(); }
};

int main()
{
	std::aligned_storage<sizeof(MyClass), alignof(MyClass)>::type storage;
	std::unique_ptr<MyClass, DeleterCallingDestructor> ptr(new(&storage) MyClass);
	ptr->DoSomething();
	return 0;
}
Тут у вас три раза употребляется MyClass. Что вы предлагаете с этим кодом делать? Копировать? Но тогда вы легко можете изменить только одно упоминание, оставив остальные два неизменными. И это будет работать или падать в зависимости от состояния звёзд.

Чтобы это работало надёжно — это всё нужно завернуть в какую-нибудь конструкцию, параметризованною типом MyClass. Про что, собственно, и статья :-)
Я писал конкретно про placement new и автоматический вызов деструктора. Это всё-таки пример, заворачивание в какую-нибудь параметризованную конструкцию дало бы лишь на одну конструкцию, в которую нужно вникать при чтении, больше.
Ну это понятно. Я думаю во всём, что описано в статье рулит rule of three. Ваш placement new — не исключение :-)
Очевидно, что это можно оформить в виде шаблонного класса с интерфейсом подобным смарт-пойнтерам — какой-нибудь StackObject. Я у себя в проекте так и сделал. Помимо экономии на аллокациях памяти, объект можно создавать или не создавать по условию и не заботиться потом об удалении.
«Динамические объекты с отношением родитель-ребёнок» может проще назвать это паттерн компоновщик?
Эти выражения не взаимозаменяемые, Компоновщик можно реализовать без динамических объектов и родитель-ребёнок может не быть Компоновщиком.
Так то оно так, но идет упоминание в контексте Qt. А в Qt сделано «классически» (как у GoF).
std::make_unique появился только в c++14,

std::shared_ptr::reset() и std::unique_ptr::reset() созданы для использования с оператором new()

Даже если кто-то что-то и сказал в прошлом, не надо считать это догмой вне зависимости от контекста и предметной области.

Не надо пугаться использовать new(), надо пугаться не уметь делать вменяемый дизайн.
std::make_unique появился только в c++14

В статье говорится о современном С++. Современный С++ на данный момент и есть С++14.

std::shared_ptr::reset() и std::unique_ptr::reset() созданы для использования с оператором new()

Если быть более точным, они созданы для работы с владеющими указателями. Оператор new — один из способов такой указатель создать. Так или иначе, если в С++ есть инструмент Х, это не значит, что им нужно обязательно пользоваться. В С++ вообще такая философия, что давайте сначала накидаем инструментов, а потом посмотрим, как оно полетит. Наберёмся опыта и выработаем гайдлайны, как этим на самом деле нужно пользоваться. Вот можно ловить исключения по значению, но так делать не надо, потому как слайсинг, исключение при копировании исключения и всё такое. Можно вернуть из функции константный объект по значению, но это не имеет смысла и может скорее только навредить. Можно сделать виртуальную функцию шаблонной или добавить её аргументам значения по умолчанию, но проблем не оберёшься потом. Можно объявить неконстистентные операторы сравнения, но тогда их уже нельзя будет назвать операторами сравнения. Примеров можно много набрать. Поэтому сейчас и развиваются C++ Core Guidelines. Убрать какой-то инструментарий из языка мы не можем — обратная совместимость дело святое. Остаётся оградить разработчиков от неразумного использования существующего инструментария. Именно от неразумного. Т.к. под каждый пример при желании можно подобрать специфическую ситуацию, где следует отойти от гайдлайнов. Но это другой разговор. Это особенные случаи. И в статье я указал на факт, что в особенных ситуациях без использования new и delete нам не обойтись.

Даже если кто-то что-то и сказал в прошлом, не надо считать это догмой вне зависимости от контекста и предметной области.

Простите, это вы о чём конкретно?

Не надо пугаться использовать new(), надо пугаться не уметь делать вменяемый дизайн.

Не вижу, как это связано. По-моему, это ортогональные вещи.
В статье говорится о современном С++. Современный С++ на данный момент и есть С++14.

Простите, но для меня, например, современный язык — это тот, который использует отрасть. Нельзя, в общем случае, просто так взять и перейти на c++14. Если компания разрабатывает продукт целиком и полностью — это одно дело. Если, как например в automotive, для создания одной машинки задействованы сотни OEM поставщиков, то данная статья даже вредна для неокрепших умов юниоров.

Простите, это вы о чём конкретно?

я про это:
Саттер и Майерс в своё время всё разложили по полочкам


Избегайте использования new и delete в коде. Воспринимайте их как низкоуровневые операции ручного управления динамической памятью.

std::shared_ptr<A> a;
....
a = std::make_shared<A>();

Не пугайтесь и не пугайте юниоров использовать new() в коде:
std::shared_ptr<A> a;
....
a.reset(new A()); 

но для меня, например, современный язык — это тот, который использует отрасть

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

Даже если кто-то что-то и сказал в прошлом, не надо считать это догмой вне зависимости от контекста и предметной области

Я не хотел называть это догмами. Без фанатизма. Да, С++ отличается широким применением и, как следствие, сильно варьируется для разных контекстов и предметных областей. Однако, можно выделить некоторые общие рекомендации, которые работают в большинстве случаев. Например, с мажорными компиляторами. Такие рекомендации вполне можно считать умолчаниями. Если в конкретном контексте или области они неприменимы — без проблем. Просто нужно понимать, почему они не применимы. Возможно, в связи со спецификой конкретного компилятора или платформы (например, не реализованы определённые оптимизации, не поддерживаются определённые инструменты). Возможно, в связи со спецификой предметной области (например, жёсткое ограничение на время отклика системы, которое требует абсолютной прозрачности потока выполнения). Но умолчания это не меняет.

Не пугайтесь и не пугайте юниоров использовать new() в коде: a.reset(new A());

В качестве исключения — до, конечно, можно. По умолчанию — нет. И в статье описано, почему.
вместо ptr.reset(new MyClass()) можно сразу делать ptr = make_shared(), например.
Есть определённая беда в C++11, которая периодически заставляет делать так:

data::Value *data = new data::Value(std::move(val));
thread.perform([this, data] () -> bool {
updateFromData(*data);
delete data;
return true;
});

В С++14 беда исправлена (можно перемещать внутрь захвата лямбды), но он ещё не везде есть.
Да, верно, такая проблема есть. Если вы вынуждены использовать С++11, следует хотя бы избавиться от delete:
auto* data = new data::Value(std::move(val));
thread.perform([this, data] () -> bool {
  std::unique_ptr<data::Value> dataPtr(data);
  updateFromData(*data);
  return true;
});

Другой вариант — использовать shared_ptr. Во многих случаях вполне себе вариант. Время работы нити обычно несравнимо больше времени копирования shared_ptr.
В С++14, действительно, можно использовать init capture. Оставлю здесь пример для полноты:
auto data = std::make_unique<data::Value>(std::move(val));
thread.perform([this, data = std::move(data)]() -> bool {
  updateFromData(*data);
  return true;
});
вот только из-за того, что в лямбде захвачен некопируемый объект, сама лямбда тоже становится некопируемой. И в std::function, например, её уже не засунешь.
Хорошее замечание, спасибо. Такая проблема есть, универсального решения нет. Больше информации по ссылкам:
http://stackoverflow.com/q/25330716/261217
http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2014/n4159.pdf
А есть под с++ какой-нибудь DI Container, с подсчетом ссылок, скоупами и т.д?, который мог бы:
1) давать нам объекты без оператора new
2) следить за зависимостями у выполнять роль некоего GC
3) содержать ральзичные стратегии выделения памяти

Зачем? Вы поличите в итоге Java, на котором ещё и писать неприятно. С хорошим дизайном первые два пункта просто не нужны. Последний пункт частично реализован в STL (см. параметр allocator у контейнеров).

Не могли бы Вы объяснить человекому, далекому от C++, какой такой хороший дизайн позволяет отказаться от первых двух пунктов — читай, DI?
  1. Шаблоны и статический полиморфизм (policy-based design).
  2. RAII.

Всё это отлично описано в книге Modern C++ Design. Да, реализации GC тоже есть, но нужны они в 0.01% случаев.

Ни одна из этих трех задач не является чем-то, чем должен заниматься DI Container

После появления стандартных умных указателей с большой опаской юзаю new и delete и трудно понимаю зачем в той или иной документации описано обязательное их использование. Исключение, конечно, Qt, но это относится к их QClass объектам, не к моему коду.

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

Но меня всегда смущали лишние операции при создании/удалении объектов (особенно при удалении, где есть проверка на ref_count, то есть ветвление). Умом понимаю: всё от проклятого желания переоптимизировать код. Но из-за этого часто так и тянет ручное управление использовать где стоит и где не стоит.

Достаточно усвоить одно правило: заниматься оптимизацией нужно только по данным профайлера. Если что-то тормозит — запускаешь профайлер и смотришь, что именно. Для того, чтобы не тормозило, нужно разумно использовать алгоритмы. А низкоуровневые оптимизации нужно использовать только тогда, когда всё упирается в алгоритмы (впрочем, большого выигрыша в такой ситуации всё равно не получишь, как правило).


Вообще, у многих пишущих на C++ прямо паранойя какая-то в отношении скорости работы кода. Да и я, когда начал разрабатывать под Android, поначалу параноил по поводу тормозной Java. Потом успокоился, когда заметил, что в подавляющем большинстве случаев тормоза были вызваны небрежно написанным кодом уровня студента третьего курса.

впрочем, большого выигрыша в такой ситуации всё равно не получишь, как правило
Не знаю кто придумал это правило и чем он его мотивировал. Из моей практики: код написанный с присмотром «вполглаза» в сгенерированный ассемблер и код, написанный с подходом «если что-то тормозит — запускаешь профайлер и смотришь, что именно» до запуска профайлера отличаются по скорости раз в 10-20, после запуска и «сшибания верхов» — раза в 2-3.

Вообще, у многих пишущих на C++ прямо паранойя какая-то в отношении скорости работы кода.
Что, как бы, логично. С++ — сложный, хитрый, опасный инструмент. Если вы не добиваетесь при его использовании максимума производительности и готовы терпеть замедление раза в 2-3, то, может быть, стоит взять что-то другое? Java, C#, Go, в конце-концов?
К сожалению, make_unique появился только в C++14, так что людям, которые еще не перешли на него, приходится использовать std::unique_ptr ptr = new T(...);
Не совсем понимаю, почему вы разделяете С++14 и С++11? По моему опыту, люди либо используют современные компиляторы, где уже есть (почти) полная поддержка С++14, либо застряли в прошлом веке, где и С++11 не пахнет.

Так или иначе, вполне можно без палева подпихнуть в std свою реализацию make_unique (см. http://stackoverflow.com/a/12580468/261217):
namespace std
{
  template<typename T, typename... Args>
  std::unique_ptr<T> make_unique(Args&&... args)
  {
    return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
  }
}

Когда компилятор будет наконец обновлён, её нужно будет удалить, а весь код будет работать точно так же. Когда мы в прошлой компании некоторое время застряли на Visual Studio 2010, мы ровно так и поступили.
Ну это какие-то розовые очки. К примеру, вот у нас самый распоследний CentOS 7, с ним идет gcc 4.8. Поддержкивается почти весь C++11, поддержки C++14 нет вообще. Вот у нас есть Visual Studio 2013 — снова то же самое: большая часть C++11 есть (по крайней мере те части, что и хотелось бы иметь), С++14 нет совсем.
Подпихнуть свои костыли можно, но это в реальных проектах дело не вполне тривиальное — надо во все файлы включать один общий хедер. Чем это городить, проще уж new воткнуть
Visual Studio 2013 поддерживает make_unqiue. Если говорить о Visual Studio, то Visual Studio 2015 Update 2 уже имеет вполне неплохую поддержку С++14. Думаю, имеет смысл смотреть именно на последнию версию в рамках конкретного компилятора.

> надо во все файлы включать один общий хедер
Достаточно часто такие хедеры имеются уже готовые. Какие-нибудь самые базовые, утилитарные. Если есть pch — ещё проще. Но, конечно, всё индивидуально. Только вы сами можете решить, какое решение лучше подходит в вашем случае. Со своей стороны я лишь надеюсь, что смог собрать в одном месте достаточно информации, чтобы можно было взвесить за и против.
К примеру, вот у нас самый распоследний CentOS 7, с ним идет gcc 4.8.
Уже как минимум есть GCC 4.9, думаю скоро и GCC 5 будет доступен как в RHEL 7.

Подпихнуть свои костыли можно, но это в реальных проектах дело не вполне тривиальное — надо во все файлы включать один общий хедер
Зачем? include_next никто не отменял.

В общем кто хочет работать — ищет способы, кто не хочет — ищет причину.
Про devtoolset слышал, но не очень в курсе, как обстоят дела с установкой собранных таким образом программ на другие машины. Тащить devtoolset как пререквизит не комильфо.
Если еще учесть, что у нас помимо gcc/linux/x86 есть еще кросскомпиляция на gcc/linux/arm и компиляция msvs/windows/x86 (и куча third-party библиотек, не дающих обновиться на visual studio 2015), то апгрейд компилятора становится задачей, требующей очень серьезного подхода. А ограничения имеющихся компиляторов дают о себе знать — то в одном regex криво работает, то в другом инициализация статических локальных переменных не атомарна, и т.д.
Костыль с include_next и /FI — в принципе, решение; если понадобится, то будем использовать.
Про devtoolset слышал, но не очень в курсе, как обстоят дела с установкой собранных таким образом программ на другие машины.
Ставите, запускаете. Никаких дополнительных пакетов ставить не нужно: рантайм он использует старый (разница между, скажем, GCC 4.4 из RHEL 6 и GCC 5.3 там оформлена как статическая библиотека, вкомпилируемая в вашу программу). Тем devtoolset и отличается от обычного GCC, собранного «руками», собственно.
Вот, собственно, скромный patch'ик на ~8000 строк. Выкидывает из libstdc++.so всё лишнее и превращает её в libstdc++_nonshared.a содержащую только «добавку» к системной libstdc++.so.6

А вместо symlink'а libstdc++.so в devtoolset — это linker script, автоматически вытаскиващий что возможно из libstdc++.so, а оставшееся — из libstdc++_nonshared.a… Таким образом вы можете получить все фичи C++ 14 (ну… почти: там кой-какие баги есть, так что Boost.Hana не поиспользуешь) с рантаймом от GCC 4.8 (или даже, при необходимости, GCC 4.4 — для совместимости с RHEL 6).

Думаю что и с кросскомпиляторами можно сделать то же самое — было бы желание.
У вас немного однобокий опыт.

В реальном мире иногда требуется иметь возможность для копиляции под разные платформы и с разными тулчейнами. Эти требования приходят со стороны заказчика: «вот вам железо, вот вам компилятор, запустите свое рефренс приложение и мы посмотрим, будем ли мы с вами заключать контракт» (утрированно).

Поэтому и сидят в CI инструментах различные задачи под несколько компиляторов и несколько платформ и никто в здравом уме не делит мир на черное (застряли в прошлом веке) и белое (современные компиляторы) с юношеским задором.
std::unique_ptr ptr = new T(...);
Надеюсь тут опечатка)

make_unique можно использовать уже сейчас https://isocpp.org/files/papers/N3656.txt
Майерс отлично прокомментировал это свойство программистов на С++ в своём докладе Why C++ Sails When the Vasa Sank.
Кстати, на Хабре была статья с основными тезисами этого доклада: https://habrahabr.ru/company/infopulse/blog/227529/.
При программировании на С++ с использованием безопасных указателей ощущения не те…
Вопрос, наверное глупый, но почему вместо int[] использовать std::vector, а не std:array<int, N>?
Основной поинт в том, что не надо использовать int[]. В этом суть. А то, что вместо него надо использовать vector, — это просто альтернатива, которая первой приходит в голову. Естественно, для массивов фиксированного размера можно использовать std::array. Можно даже отдельную статью написать о том, что для фиксированных массивов вместо vector нужно использовать std::array.
Это совершенно разные контейнеры. std::vector — буфер выделяется в куче, размер неизвестен на этапе компиляции. std::array — буфер выделяется на стеке, размер должен быть известен на этапе компиляции.
Ну, справедливости ради, данные в std::array могут жить как на стеке, так и на куче (например, если std::array используется как поле в классе, экземпляр которого размещения в куче).

Да, вы, разумеется, правы, этот момент я отметил в статье. Здесь я рассуждал так: человек задаёт вопрос, почему нельзя использовать std::array вместо std::vector => он упустил некоторые основы => делать подобные уточнения смысла нет, надо кратко отметить, в чём разница, чтобы было понятно.

У вас список сокращённый, в полном списке вы так же не пишете:


  1. Код деструктора. Если что-то нужно освобождать в деструторе, значит это что-то ранее было инициализировано, что по сути снова new/delete.
  2. Код копирующего конструктора. Правило трём(пяти), и опять же — если что-то нужно нетривиально копировать, значит стоит задуматься о дизайне.
Конструкторы и деструкторы в первую очередь нужны для работы с внешними ресурсами. Некоторые внешние ресурсы (например, файловые дескрипторы) копируются нетривиальным образом. Большинство не копируются никак. Хороший подход: заворачивать такие вещи в RAII. Вне RAII стоит избегать нетривиальных конструкторов и деструкторов. А с введением значений по умолчанию для членов класса и поддержки для них агрегатных инициализаторов (С++14) и конструкторы по умолчанию лучше оставить компилятору.

Ещё вариант, где от реализации конструкторов с деструкторами не отказаться: классы, реализующие union. Но этот инструмент очень злой и коварный, можно легко отстрелить ногу в нескольких местах по самую шею. Как правило, union это оптимизация на уровне укладки данных в памяти, что суть высшие материи, и относится скорее к чистому C.

Если поле класса не копируется, что скорее всего и класс копироваться не должен. Исключения есть везде, это естественно.

Пусть меня заминусуют, но статья должна называться «Почему НЕ НАДО программировать на C++». ;-)
У меня есть живой проект, который начали, кажется, еще в 2002 (или 2001?) году на Qt 1.45. Я его перетаскиваю потихоньку от версии к версии, постепенно созревая, чтобы переписать его с нуля. Если бы я лез к нему в потроха с появлением каждого нового стандарта языка — то уже переписал бы минимум дважды.
Для короткоживущего проекта (3-5 лет) С++ подходит замечательно. Для долговременного (LTS) — язык слишком изменчив, чтобы писать на нем код, который будет работать через 10-15 лет, без серьезных вмешательств.
И с жабой (Java) та же история, и много еще с чем.
Поэтому я ушел на pure C, к которому цепляю через обертки библиотеки.
Программа на С++/Qt согласен. Но неужели программа на С++ без использования сомнительных библиотек не соберется современными компиляторами?
Есть большой шанс, что нет. При портировании программы, написанной в 2003-2007 годах, мне пришлось, кроме всего прочего (Qt), править шаблоны, ибо их специализации перестали видеться при линковке.

Точно не соберётся, если поставить -Wall -Werror, как любят делать гордые опенсорсы :)
Я тоже работал с похожими проектами, и тут у меня есть несколько замечаний к вашему комментарию.

Для начала, зачем вообще перетаскивать в него новые фишечки плюсов? Это legacy code, либо он работает, и вы его не трогаете, либо пишете новый по всем современным канонам. Делать микс — это самое большое зло.

Далее, 2002 год. Код на плюсах в 2002 году не так уж сильно отличался от кода на C. Во всяком случае, прогали в основном в стиле «С с классами». Поэтому когда я слышу «проект 2002» года — это скорее всего намешанные malloc и new, процедурный стиль, венгерская нотация, конструкции вида ** и *& и все в таком же духе.

Насчет LTS. Это не так. Основные претензии к мастодонтам, написанные на C — как во всем этом разобраться, если ты не бородатый дядька, который все это написал? Поэтому и начинают написать всякие проекты типа clang, призванные вытеснить gcc.

Самое сложное искусство на С++, к которому, как кажется мне, все и придираются — как писать средний код. Средний — в том плане, что в нем и новичок сможет довольно быстро разобраться, и гуру не будет плеваться. Современный C++ как раз движется в этом направлении, и это прекрасно. Все эти function, lambda, smart pointers и прочее — как раз помогают писать красивый, понятный и лаконичный код, без всяких трюков и низкоуровневых хаков. При том, что в частный случаях все трюки и хаки еще по старой совместимости доступны. По-моему, это отлично.
> Для начала, зачем вообще перетаскивать в него новые фишечки плюсов?
Гм. Я, вроде бы, такого не говорил. :) Хотя, я, конечно, пытался сделать микс, но вовремя остановился.

> Все эти function, lambda, smart pointers и прочее — как раз помогают писать красивый, понятный и лаконичный код
При условии правильного проектирования даже не базовых классов, а базовых шаблонов.
Простой пример — автомобиль. 4 колеса, кузов, двигатель внутреннего сгорания… опаньки! тесла-мобиль… что делать будем?
Если мы не заложились в самом начале на существование других двигателей, кроме ДВС, то мы попали — у нас рвется шаблон. :)
Если заложились — то мы влипаем в класс engine по полной программе.

> Основные претензии к мастодонтам, написанные на C — как во всем этом разобраться, если ты не бородатый дядька, который все это написал? Поэтому и начинают написать всякие проекты типа clang, призванные вытеснить gcc.
Ну, как по мне, то наилучшим аргументом за C++ был бы компилятор с C++, написанный (включая runtime) только на C++.
Гм. Я, вроде бы, такого не говорил. :) Хотя, я, конечно, пытался сделать микс, но вовремя остановился.

Ну вы говорили, что вот вышел стандарт — можно лезть и переписывать. А я говорю — нафига?

Простой пример — автомобиль.

Плохой пример, из которого вообще никаких проблем не видно. Решений может быть море, начиная от шаблонной магии (мы же знаем, какие характеристики нужны нам для расчетов) до нескольких реализации ДВС, наследуемых от базового класса движка.
Конечно, нужно думать и проектировать. И это на любом языке. На С, если прогать по принципу «фигак-фигак и в продакш», происходит тоже самое — через некоторое время вообще непонятно, как это поддерживать.

Ну, как по мне, то наилучшим аргументом за C++ был бы компилятор с C++, написанный (включая runtime) только на C++.

clang?
Ну, как по мне, то наилучшим аргументом за C++ был бы компилятор с C++, написанный (включая runtime) только на C++.

Микрософт, кстати, не так давно переписал свою CRT на С++. Так что уже свершилось. (Ну и да, даже GCC уже некоторое время написан частично на C++)

Две самые популярные операционки (iOS/MacOS и Android) так и устроены. Там даже базовая библиотека (начиная со всяких загрузчиков и прочего) на C++.
А причём здесь С++ — это ведь Qt штампует новые версии. Был бы Qt написан на C проблемы были бы те же.
До С++11 даже проблем с дунгрейтом практически не было, если знаешь C++ ошибки быстро исправишь.
К примеру, один из моих проектов должен работать и в одной старой ОСи, в которой только только gcc 2.9*. Неудобно конечно, но код( со всеми плюшками) компилируется как с новым gcc, так и со старым.
Речь идет о том, что код, написанный 15 лет назад на c++ почти наверняка не скомпилируется современными компиляторами.
В то время как чистый Си — скорее всего выдаст несколько варнингов.

зы. На самом деле мне больше всего не нравится, когда набор библиотек языка становится его частью. Этакий VendorLock. А C++ идет по этому пути очень радостно (или у меня паранойя разыгралась? :)

Я пользуюсь C++ проектом который начали писать в 1989. Код местами выглядит странно по сегодняшним меркам, но все компилируется. Как вам уже много раз говорили, в вашем случае проблема в Qt а не в C++. В С++ несовместимых изменений практически не было.

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

А сейчас что, вдруг стала нелегальной?

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

Все стандартные классы лежат в std::, поэтому не могут вызвать проблем, если не делать using namespace std;, что всегда считалось плохой практикой.

Вот это, кстати, одна из больших проблем: в std:: они переехали в 98м году, с выходом стандарта C++98. С более ранними версиями совместимость действительно… не очень — там, в частности, все эти вещи лежали в глобальном namespace (потому что других не было).

Ну так и программы, написанные на C до ~85го года (до выхода проектов ANSI C стандарта) вы современными компиляторами, зачастую, собрать не сможете. В чём разница?
Ну, лично я готов злоупотребить плохой практикой если увижу, что блок кода наполовину состоит из std:: (в хедер, однако, ни-ни). И рассчитывать на то, что 15 лет назад рандомный коллега не зафигачил в проект using namespace std я бы точно не стал

Сделайте using namespace на уровне данной функции или вообще на уровне блока. Вызванные потенциальные проблемы будут минимальны.

вот когда я прознал о том, что using namespace вполне делается внутри фунции, жить действительно стало чуточку проще
И рассчитывать на то, что 15 лет назад рандомный коллега не зафигачил в проект using namespace std я бы точно не стал.
Ага.

А если он вот такой код зафигачит:
char *safe_malloc(size)
unsigned size;
{
    char *p;

    extern char *malloc();

    if ((p = malloc(size)) == (char *) 0) {
        cleanupHandler(heap_no_mem,"safe_malloc");
    }
    return p;
}
Пример из реального проекта, если что… теперь и C тоже не использовать? В машинных годах писать?
моя изначальная мысль заключалась в том, что с годами в коде на плюсах не должно ничего ломаться…
Давайте я вам процитирую вашу же «начальную мысль»:
Для короткоживущего проекта (3-5 лет) С++ подходит замечательно. Для долговременного (LTS) — язык слишком изменчив, чтобы писать на нем код, который будет работать через 10-15 лет, без серьезных вмешательств.

Поэтому я ушел на pure C, к которому цепляю через обертки библиотеки.
Так вот это — бред. 100% совместимость может быть только со 100% неизменяемым язком. На любое изменение всегда можно придумать код, который будет этим изменением сломан.

Код, который написан на С или C++ аккуратно — работает отлично и через 10 лет и через 20. Код, который, я извиняюсь, написан через жопу — не работает ни там, ни там.

Так что совершенно неясно — чего вы добиваетесь переходом с С на C++.
100% совместимость может быть только со 100% неизменяемым язком. На любое изменение всегда можно придумать код, который будет этим изменением сломан

Здесь вы расширение языка не называете изменением?

Называю, конечно. Но вы когда-нибудь на autoconf-скрипты смотрели? Они бывают как позитивными (если что-то компилируется/работает, то делаем так), так и негативными (если что-то не компилируется, то делаем эдак). Любое изменение может поломать негативный тест (просто по определению), но даже позитивные могут быть сломаны если они сделаны через «одно место» (например залазят в «зарезервированные» индентификаторы типа _Bool).
Речь идет о том, что код, написанный 15 лет назад на c++ почти наверняка не скомпилируется современными компиляторами.
В то время как чистый Си — скорее всего выдаст несколько варнингов.
Вы это проверяли? Или так — вещаете о том, о чём понятия не имеете?

Я, среди прочего, гоняю на работе SPEC2000 и SPEC2006. Которые, как несложно догадаться, появились примерно как раз ~15 лет назад. Количество костылей, которые нужно пристраивать со всех сторон к тестам на C и на C++ — вполне сравнимо.

И во всех случаях это — результат использования плохого стили. Чем программа на C объявляющая внутри функции malloc вместо того, чтобы заголовочный файл включить (300.twolf) или не использующая include guard'ов (253.perlbmk), в сущности, отличается от программы на C++ с using namespace std (252.eon)? Да ничем: во всех случаях кому-то нужно было очень сильно надо «дёшево» «заткнуть проблему» — а через 15 лет это аукается… Вряд ли можно это отнести к недостаткам языка: «свинья грязь везде найдёт»…

На самом деле мне больше всего не нравится, когда набор библиотек языка становится его частью.
А чем это плохо, собственно? Посмотрите на PHP: то, что там куча всего-всего-всего прямо сразу из коробки — всем, скорее, нравится, не нравится то, что это всё-всё-всё свалено в кучу без всякой системы… К тому же Python'у (где библиотек лишь немногим меньше, но они более-менее разумно организованы) претензий нет вообще.
То, что идеальный код, использующий new/delete будет работать быстреe авто-оберток, думаю очевидно.
То, что идеальный код, использующий new/delete написать сложно, тоже очевидно.
Поэтому в 99% случаев авто-обертки использовать гораздо разумнее.

Но я поспорю с автором.
New/delete — это не враги, а наши очень влиятельные друзья, к помощи которых мы можем прибегать только в серьезных ситуациях. C++ без new/delete, malloc/free — это уже не C++, а хрень на постном масле. Я надеюсь, что очередной 0xXX не забудет, что C++ задумывался как язык для упрощения работы с реальным железом, так сказать высокоуровневый ассемблер… а не солянка из кучи синтаксических наворотов. Думаю, что в скором будущем 0xXX неизбежно выделится в новый язык, как в свое время C превратился в C++, ибо тем, кто глаголит на старославянском, не с руки писать один код с теми, кто не чтит мудрость предтечей :)
То, что идеальный код, использующий new/delete будет работать быстреe авто-оберток, думаю очевидно.

Тот же unique_ptr соберётся в точно такой же машинный код, что и new/delete.


0xXX неизбежно выделится в новый язык

Да выделился уже: D. Просто он почти никому не нужен.

Тот же unique_ptr соберётся в точно такой же машинный код, что и new/delete
— Вы это лично проверяли?

Вот c++ / ассемблерный код, который выдает C++Builder XE3 для auto_ptr.

struct MyStruct
{
int x;
int y;
};

MyStruct* p = new MyStruct;
delete p;

std::auto_ptr ap(new MyStruct);
ap.reset();

832: MyStruct* p = new MyStruct;
00423BE6 6A08 push $08
00423BE8 E80F7C0400 call $0046b7fc
00423BED 59 pop ecx
00423BEE 8945CC mov [ebp-$34],eax

833: delete p;
00423BF1 FF75CC push dword ptr [ebp-$34]
00423BF4 E8F77B0400 call $0046b7f0
00423BF9 59 pop ecx

835: std::auto_ptr ap(new MyStruct);
00423BFA 66C745E80C00 mov word ptr [ebp-$18],$000c
00423C00 6A08 push $08
00423C02 E8F57B0400 call $0046b7fc
00423C07 59 pop ecx
00423C08 50 push eax
00423C09 8D55FC lea edx,[ebp-$04]
00423C0C 52 push edx
00423C0D E83A000000 call std::auto_ptr::auto_ptr(MyStruct *)
00423C12 83C408 add esp,$08
00423C15 FF45F4 inc dword ptr [ebp-$0c]
00423C18 66C745E81800 mov word ptr [ebp-$18],$0018

836: ap.reset();
00423C1E 6A00 push $00
00423C20 8D4DFC lea ecx,[ebp-$04]
00423C23 51 push ecx
00423C24 E883000000 call std::auto_ptr::reset(MyStruct *)
00423C29 83C408 add esp,$08

Думаю разница теперь Вам очевидна?
К сожалению, unique_ptr данная версия компилятора не поддерживает, поэтому если Вы замените auto_ptr на unique_ptr и привидете ассемблерный код своего компилятора, будет интересно посмотреть.
Хабр удалил из примера кода все знаки и содержимое > <, надеюсь сможете расставить сами.
Вы, скорей всего, при компиляции оптимизацию не включили, вот у вас ничего и не заинлайнилось.
Нет, я проверял, не в этом дело. MSVS 2015 с включенной оптимизацией дает более краткий код, но все равно длиннее чем new/delete
По шаблонам можно проследить что, к примеру, unique_ptr всегда делает лишнюю проверку вида

if (data != nullptr)
  delete data;


тогда как прямой вызов delete очевидно этой проверки не проводит. Это связано с желанием авторов unique_ptr правильно хэндлить всякие ситуации типа такой

mystruct *x = new mystruct;
std::unique_ptr<mystruct> p(x);
p.reset(x); // здесь выполняется проверка что в reset не передали тот же самый указатель
По шаблонам можно проследить что, к примеру, unique_ptr всегда делает лишнюю проверку вида
if (data != nullptr)
  delete data;
Зачем оно там? Читаем спецификацию на operator delete: указатель на область памяти для освобождения или нулевой указатель. Если вы находите подобный код «по шаблонам», то это значит только одно: разработчики вашего компилятора малость налажали и/или немного слишком сильно перестраховались.

Я дополнил ваши несколько строк до полной программы:
здесь
#include <memory>

struct MyStruct
{
int x;
int y;
};

extern void foo(MyStruct *);

void bar() {
  MyStruct* p = new MyStruct;
  delete p;
}

void baz() {
  std::unique_ptr<MyStruct> ap(new MyStruct);
  ap.reset();
}
Результат при использовании нормального компилятора (а не недоразумений от фирм, которые на оптимизации давно забили болт):
тут
$ g++ -O3 -std=c++14 -fno-exceptions -S test.cc -o- | c++filt
	.file	"test.cc"
	.section	.text.unlikely,"ax",@progbits
.LCOLDB0:
	.text
.LHOTB0:
	.p2align 4,,15
	.globl	bar()
	.type	bar(), @function
bar():
.LFB1818:
	.cfi_startproc
	subq	$8, %rsp
	.cfi_def_cfa_offset 16
	movl	$8, %edi
	call	operator new(unsigned long)
	movl	$8, %esi
	movq	%rax, %rdi
	addq	$8, %rsp
	.cfi_def_cfa_offset 8
	jmp	operator delete(void*, unsigned long)
	.cfi_endproc
.LFE1818:
	.size	bar(), .-bar()
	.section	.text.unlikely
.LCOLDE0:
	.text
.LHOTE0:
	.section	.text.unlikely
.LCOLDB1:
	.text
.LHOTB1:
	.p2align 4,,15
	.globl	baz()
	.type	baz(), @function
baz():
.LFB1819:
	.cfi_startproc
	subq	$8, %rsp
	.cfi_def_cfa_offset 16
	movl	$8, %edi
	call	operator new(unsigned long)
	movl	$8, %esi
	movq	%rax, %rdi
	addq	$8, %rsp
	.cfi_def_cfa_offset 8
	jmp	operator delete(void*, unsigned long)
	.cfi_endproc
.LFE1819:
	.size	baz(), .-baz()
	.section	.text.unlikely
.LCOLDE1:
	.text
.LHOTE1:
	.ident	"GCC: (GNU) 5.3.0"
	.section	.note.GNU-stack,"",@progbits
Что называется — найдите 10 отличий.

Но что будет если структуру для чего-нибудь поиспользовать?
Вот так
#include <memory>

struct MyStruct
{
int x;
int y;
};

extern void foo(MyStruct *);

void bar() {
  MyStruct* p = new MyStruct;
  foo(p);
  delete p;
}

void baz() {
  std::unique_ptr<MyStruct> ap(new MyStruct);
  foo(ap.get());
  ap.reset();
}
Получаем
вот это
$ g++ -O3 -std=c++14 -S test2.cc -o- | c++filt
	.file	"test2.cc"
	.section	.text.unlikely,"ax",@progbits
.LCOLDB0:
	.text
.LHOTB0:
	.p2align 4,,15
	.globl	bar()
	.type	bar(), @function
bar():
.LFB1860:
	.cfi_startproc
	pushq	%rbx
	.cfi_def_cfa_offset 16
	.cfi_offset 3, -16
	movl	$8, %edi
	call	operator new(unsigned long)
	movq	%rax, %rbx
	movq	%rax, %rdi
	call	foo(MyStruct*)
	movq	%rbx, %rdi
	movl	$8, %esi
	popq	%rbx
	.cfi_def_cfa_offset 8
	jmp	operator delete(void*, unsigned long)
	.cfi_endproc
.LFE1860:
	.size	bar(), .-bar()
	.section	.text.unlikely
.LCOLDE0:
	.text
.LHOTE0:
	.section	.text.unlikely
.LCOLDB1:
	.text
.LHOTB1:
	.p2align 4,,15
	.globl	baz()
	.type	baz(), @function
baz():
.LFB1861:
	.cfi_startproc
	.cfi_personality 0x3,__gxx_personality_v0
	.cfi_lsda 0x3,.LLSDA1861
	pushq	%rbp
	.cfi_def_cfa_offset 16
	.cfi_offset 6, -16
	pushq	%rbx
	.cfi_def_cfa_offset 24
	.cfi_offset 3, -24
	movl	$8, %edi
	subq	$8, %rsp
	.cfi_def_cfa_offset 32
.LEHB0:
	call	operator new(unsigned long)
.LEHE0:
	movq	%rax, %rdi
	movq	%rax, %rbx
.LEHB1:
	call	foo(MyStruct*)
.LEHE1:
	addq	$8, %rsp
	.cfi_remember_state
	.cfi_def_cfa_offset 24
	movq	%rbx, %rdi
	movl	$8, %esi
	popq	%rbx
	.cfi_def_cfa_offset 16
	popq	%rbp
	.cfi_def_cfa_offset 8
	jmp	operator delete(void*, unsigned long)
.L5:
	.cfi_restore_state
	movq	%rax, %rbp
	movq	%rbx, %rdi
	movl	$8, %esi
	call	operator delete(void*, unsigned long)
	movq	%rbp, %rdi
.LEHB2:
	call	_Unwind_Resume
.LEHE2:
	.cfi_endproc
.LFE1861:
	.globl	__gxx_personality_v0
	.section	.gcc_except_table,"a",@progbits
.LLSDA1861:
	.byte	0xff
	.byte	0xff
	.byte	0x1
	.uleb128 .LLSDACSE1861-.LLSDACSB1861
.LLSDACSB1861:
	.uleb128 .LEHB0-.LFB1861
	.uleb128 .LEHE0-.LEHB0
	.uleb128 0
	.uleb128 0
	.uleb128 .LEHB1-.LFB1861
	.uleb128 .LEHE1-.LEHB1
	.uleb128 .L5-.LFB1861
	.uleb128 0
	.uleb128 .LEHB2-.LFB1861
	.uleb128 .LEHE2-.LEHB2
	.uleb128 0
	.uleb128 0
.LLSDACSE1861:
	.text
	.size	baz(), .-baz()
	.section	.text.unlikely
.LCOLDE1:
	.text
.LHOTE1:
	.ident	"GCC: (GNU) 5.3.0"
	.section	.note.GNU-stack,"",@progbits
О боги. Кода-то, кода… но погодите — весь этот код, он ведь по делу! Просто версия с delete содержит в себе ошибку (неправильно обрабатывается случай, когда foo бросает исключение), а версия с std::unique_ptr этот случай обрабатывает правильно — о чём, собственно, и статья. Ну и, конечно, если мы скажем, что исключения нас не волнуют, то:
отличий снова нет
$ ~/work/gcc5/bin/g++ -O3 -std=c++14 -fno-exceptions -S test2.cc -o- | c++filt
	.file	"test2.cc"
	.section	.text.unlikely,"ax",@progbits
.LCOLDB0:
	.text
.LHOTB0:
	.p2align 4,,15
	.globl	bar()
	.type	bar(), @function
bar():
.LFB1818:
	.cfi_startproc
	pushq	%rbx
	.cfi_def_cfa_offset 16
	.cfi_offset 3, -16
	movl	$8, %edi
	call	operator new(unsigned long)
	movq	%rax, %rbx
	movq	%rax, %rdi
	call	foo(MyStruct*)
	movq	%rbx, %rdi
	movl	$8, %esi
	popq	%rbx
	.cfi_def_cfa_offset 8
	jmp	operator delete(void*, unsigned long)
	.cfi_endproc
.LFE1818:
	.size	bar(), .-bar()
	.section	.text.unlikely
.LCOLDE0:
	.text
.LHOTE0:
	.section	.text.unlikely
.LCOLDB1:
	.text
.LHOTB1:
	.p2align 4,,15
	.globl	baz()
	.type	baz(), @function
baz():
.LFB1819:
	.cfi_startproc
	pushq	%rbx
	.cfi_def_cfa_offset 16
	.cfi_offset 3, -16
	movl	$8, %edi
	call	operator new(unsigned long)
	movq	%rax, %rbx
	movq	%rax, %rdi
	call	foo(MyStruct*)
	movq	%rbx, %rdi
	movl	$8, %esi
	popq	%rbx
	.cfi_def_cfa_offset 8
	jmp	operator delete(void*, unsigned long)
	.cfi_endproc
.LFE1819:
	.size	baz(), .-baz()
	.section	.text.unlikely
.LCOLDE1:
	.text
.LHOTE1:
	.ident	"GCC: (GNU) 5.3.0"
	.section	.note.GNU-stack,"",@progbits


В общем я поигрался с компиляторами и могу сказать, что «код с обёртками» при использовании нормальных компиляторов получается практически идентичным тому, который получается без их использования. Лет 10 назад я не очень верил, что до этого дойдёт, но… факты — упрямая вещь.

А аргумент «я очень-очень забочусь о скорости работы кода на недокомпиляторах» — он немного того, шизифренией попахивает. Если вас волнует скорость кода — зачем вы используете компилятор, в котором качество кодогенерации, как всем известно, «не ахти»? А если «не очень волнует» и «есть причины использовать именно этот, не очень качественно оптимизирующий компилятор» — то зачем ловко стрелять из-под колена встав в позу из Кама-Сутры и грозя отстерить себе чего-нибудь?
Зачем оно там?


Я же написал зачем, даже пример привел. Если в reset() тупо сделать delete без проверки на указатель, то второй приведенный пример кода сломается. Менее понятно зачем схожая проверка есть в деструкторе, но похоже это дань универсальности — в unique_ptr можно, к примеру, запихать дескриптор потока с кастомным деструктором который этот поток закрывает, или свой собственный умный указатель. Есть даже целая интересная тема на тему того что гарантированное стандартом поведение (вызывать deleter для нулевого указателя или нет) отличается для unique_ptr и shared_ptr:

http://stackoverflow.com/questions/11164354/does-the-standard-behavior-for-deleters-differ-between-shared-ptr-and-unique-ptr

А так да, в реализации STL от GCC похоже аккуратно написали специализацию для «простых» случаев и это еще раз показывает всю крутизну C++. Но к сожалению не все реализации STL столь же вылизаны.

Было бы любопытно глянуть что ICC сгенерит, но лень сейчас на рабочую машину лезть и проверять )
так в итоге внутри unique_ptr есть проверка на nullptr и внутри delete есть вторая такая же проверка. Компилятор по идее должен просто выкинуть одну из них из-за её тождественности.
Да, так и будет, потому что проверка внутри delete не скрыта в недрах какой-нибудь вызываемой функции, а вставляется компилятором прямо по месту самого delete (что логично, т.к. иначе мы не смогли бы получать преимущества от знания, что в этой точке указатель всегда ненулевой). В комментарии ниже есть искусственный пример, показывающий, что в обоих случаях код будет идентичным.
Зачем оно там?
В реальности, там в шаблоне конструкция вида:
if (get() != nullptr)
  get_deleter()(get());
для поддержки нестандартных deleter'ов (стандарт обязывает делать такую проверку и вызывать их только для ненулевых указателей). И да, в ситуациях, когда компилятор не может доказать (в примерах выше то у него всего на виду), что указатель ненулевой, проверка останется. Но и для простого delete компилятор прямо по месту такую проверку вставит, т.к., как вы верно указали, по стандарту delete от нулевого указателя абсолютно легален и должен просто не делать ничего, а вызывать, к примеру, какой-нибудь нетривиальный деструктор, передав ему нулевой указатель, не самая лучшая идея. Вот синтетический пример, в обоих случаях идет проверка.

Вообще, по логике, если мы уверены, что в этой точке указатель не нулевой, то мы можем «подсказать» это компилятору, написав какое-нибудь не делающее ничего действие с использованием разыменования этого указателя (скажем, delete &*p;). Тогда компилятор имеет право считать, что указатель всегда ненулевой, т.к. иначе происходит UB. На практике, я чуток поигрался, и компиляторы (пока?) такие подсказки не воспринимают, нужно что-то очень явное с объектом сделать (например, вывести его на экран), чтоб такая оптимизация заработала. Возможно, у кого-нибудь получится найти рабочий способ?
// здесь выполняется проверка что в reset не передали тот же самый указатель
Я не уверен, кстати, что подобная проверка (если я верно понял то, что в ситуации равенства обоих указателей при ней ничего не происходит) не нарушает стандарт. Стандарт (C++11 20.7.1.2.5p4) говорит нам о поведении reset() следующее:
Effects: assigns p to the stored pointer, and then if the old value of the stored pointer, old_p, was not equal to nullptr, calls get_deleter()(old_p).
Как видим, вышеупомянутой проверки стандарт не предусматривает, а значит в точке p.reset(x); должен быть вызван деструктор для x и освобождена память. Да, можно сказать, что дальше в момент вызова деструктора p у нас возникает UB из-за повторного удаления, и поэтому мы можем делать, что захотим. Ну а вдруг такого не будет (например, ниже мы вызовем release())?
Да, верно. Я невнимательно посмотрел MS-реализацию, там все реализовано по Стандарту и мой пример будет крэшиться.
здесь выполняется проверка что в reset не передали тот же самый указатель

Каким образом, если сравнение идёт с нулевым указателем? Собственно, на cppreference указано:


A test for self-reset, i.e. whether ptr points to an object already managed by *this, is not performed, except where provided as a compiler extension or as a debugging assert.
Хм, а ведь и правда. Невнимательно посмотрел, такой вариант работать действительно в MS STL не будет
Оптимизацию он включил. Он просто использует компиляторы, которые известны своей плохой оптицизацией. У нас были случаи, когда MSVC отказывался инйланить функции буквально в две машинных инструкции — что теперь, отказываться от C++ и возвращаться на C, используя макросы вместо типобезопасных обёрток? По мне — так уж лучше на clang перейти. Некоторые вещи в проектах на Windows можно собрать только MSVC, так как они завязаны на непереносимые расширения MSVC — ну так clang и MSVC совместимы на уровне объектников, можно делать как Chromium: собирать всё сначала clang'ом, а то, что не собралось (обычно буквально пара-тройка файлов) — уже MSVC.
Вы правы что код с обертками будет будет чуть медленнее.
Но это экономия на спичках, на практике разница будет ничтожно мала
Где-то возможно востребована даже такая экономия, но ИМХО даже если Вам нужна максимальная производительность, то в 99% случаев Вы выиграете намного больше соптимизировав что-то другое.
Спички бывают разные если вы сжигаете их миллионами, то стоит и на них поэкономить.

Тут, многие пишут, что это все не правильный компилятор и он несет не правильный мед, мол «правильный» всё оптимизирует и всё будет ровно, а самое интересно, тот компилятор который им нравится — и есть самый «правильный». Какая наивность…

Код, который был приведен выше — был абсолютно тривиальный и без включенной оптимизации, дабы показать какая реальная разница между кодом реализации new/delete и auto_ptr. Компилятор легко оптимизирует такие тривиальные примеры, но в реальных программах, где для создания/удаления объектов есть еще и условия и обращения к члена класса, он совсем не Чак Норис, и может пропустить кучу апперкотов и пару хуков от программиста.

Господа, попробуйте запустить вот этот код, и проверить «крутость» своих компиляторов ;)

struct MyStruct
{
int x;
int y;
};

void test2_work(MyStruct* in_p, int& r)
{
if(in_p->x == in_p->y)
r = in_p->x + in_p->y;
}

void test2_new(MyStruct** in_p, int i)
{
*in_p = new MyStruct;
(*in_p)->x = i;
(*in_p)->y = i;
}

void test2_delete(MyStruct** in_p)
{
delete *in_p;
}

void test2_auto(std::auto_ptr& in_ap, int i)
{
in_ap.reset(new MyStruct);

in_ap->x = i;
in_ap->y = i;
}

void test2_awork(std::auto_ptr& in_ap, int& r)
{
if(in_ap->x == in_ap->y)
r = in_ap->x + in_ap->y;
}

void test2_reset(std::auto_ptr& in_ap)
{
in_ap.reset();
}

int _tmain(int argc, _TCHAR* argv[])
{
MyStruct* p_main;
std::auto_ptr ap_main;

int r=0;
int t1, t2;
int k = 20000000;

printf(«Start test auto!\r\n»);
t1 = GetTickCount();
for (int i = 0; i < k; i++)
{
test2_auto(ap_main, i);
test2_awork(ap_main, r);
test2_reset(ap_main);
}
t2 = GetTickCount();
printf(«Result auto t=%d, r=%d\r\n», t2-t1, r);

printf(«Start test new/delete!\r\n»);
t1 = GetTickCount();
for (int i = 0; i < k; i++)
{
test2_new(&p_main, i);
test2_work(p_main, r);
test2_delete(&p_main);
}
t2 = GetTickCount();
printf(«Result new/delete t=%d, r=%d\r\n», t2-t1, r);

printf("-------- Surprize -------\r\n");

printf(«Start test auto (2)!\r\n»);
t1 = GetTickCount();
{
std::auto_ptr as(new MyStruct);
for (int i = 0; i < k; i++)
{
as->x = i;
as->y = i;
test2_awork(as, r);
}
}
t2 = GetTickCount();
printf(«Result auto (2) t=%d, r=%d\r\n», t2-t1, r);

printf(«Start test new/delete (2)!\r\n»);
t1 = GetTickCount();
{
MyStruct* s = new MyStruct;
for (int i = 0; i < k; i++)
{
s->x = i;
s->y = i;
test2_work(s, r);
}

delete s;
}
t2 = GetTickCount();
printf(«Result new/delete (2) t=%d, r=%d Fatality!!!!!!\r\n», t2-t1, r);

getch();
return 0;
}

x-----------------------------x
x------- Результат -------x
x-----------------------------x
Start test auto!
Result auto t=5203, r=39999998
Start test new/delete!
Result new/delete t=2282, r=39999998
x-------- Surprize -------x
Start test auto (2)!
Result auto (2) t=2937, r=39999998
Start test new/delete (2)!
Result new/delete (2) t=172, r=39999998 Fatality!!!

Т.е. разница больше чем в 2 раза…
Если оставить в цикле только присвоение, то компилятору и auto_ptr становится стыдно! Да, в тесте (2) можно просто объявить локальную переменную без всяких new/delete и auto_ptr, но наша цель сравнить именно их. Будет интересно увидеть результат компиляторов «конкурентов» и unique_ptr :)

А тем кто минусует мой прошлый коммент еще раз процетирую, что там написано:
То, что идеальный код, использующий new/delete будет работать быстреe авто-оберток, думаю очевидно.
То, что идеальный код, использующий new/delete написать сложно, тоже очевидно.
Поэтому в 99% случаев авто-обертки использовать гораздо разумнее.

new/delete — не враги, а друзья, которых надо использовать с умом и только если в этом есть реальная выгода. Тот, кто сомневается в своей способности их безопасно использовать и не видит пользы, может от них полностью отказаться. А тот кто не сомневается — настоящий Чак Норис :)
И да прибудет с нами сила…

auto_ptr уже deprecated. Зачем вы его используете для тестов?

У него нет unique_ptr, как я понял. Что уже само по себе говорит о качестве используемого компилятора.
Тут, многие пишут, что это все не правильный компилятор и он несет не правильный мед, мол «правильный» всё оптимизирует и всё будет ровно, а самое интересно, тот компилятор который им нравится — и есть самый «правильный». Какая наивность…
Наивность — это у вас. У нас — годы практики. Хорошо известно, что MSVC и Borland/Inprise/Embarcadero — генерируют отвратительный код. Потому что на это у них «денех нет». Разные всякие рюшечки — продать можно легко. А просто генерация более качественного кода — это не «buzzword-compliant approach». Тем более что обычно удаётся за год «отвоевать» на реальных программах 5-7%. Но 5-7% один год, второй, третий… и вдруг, лет через 10 выясняется что ваш компилятор уже неконкурентоспособен — а за эти 10 компиляторы кокурентов научились, в частности, «сворачивать» C++-обёртки и… реальные программы — уже на это закладываются!

Что делать? Ну с Embarcadero всё просто: если это — не расписка в собственном бессили, тогда что это? Обратите, кстати, внимание, на то, что BCC64 основан на clang 3.1. 3.1, Карл! Компилятор, которому недавно 4 года стукнуло! А вы не… в общем не хочу материться…

Microsoft же несколько лет назад (когда clang, наконец-то, допилили) — тоже «вдруг» прозрел и ринулся в догонку. Но пока — он ещё очень и очень отстаёт от современных качественных компиляторов.

Приличные компиляторы для x86 — это GCC и Intel сегодня. Clang хорош, но пока всё-таки отстаёт.

Компилятор легко оптимизирует такие тривиальные примеры, но в реальных программах, где для создания/удаления объектов есть еще и условия и обращения к члена класса, он совсем не Чак Норис, и может пропустить кучу апперкотов и пару хуков от программиста.
А сколько «аперкотов и хуков» пропустит программист из-за того, что ему придётся писать более сложный код? И не захочет ли он использовать алгоритмы попроще для компенсации?

А вашу программку я запустил, да. Вначале пришлось заменить GetTickCount на gettimeofday и «вдарить компилятору по почкам» использованием __attribute__((noinline)) (а то он весь ваш «сурпрайз» умудрился выкинить из программу к чёртовой матери)
Результат:
Start test auto!
Result auto t=8178878, r=399999998
Start test new/delete!
Result new/delete t=6458617, r=399999998
-------- Surprize -------
Start test auto (2)!
Result auto (2) t=470250, r=399999998
Result new/delete (2) t=488211, r=399999998 Fatality!!!!!!
Обратите внимание на ваш «suprise», кстати — но это так, лирика: современные процессоры очень плохо относятся к слишком коротким функциям — они им пайплайн сбивают и адрес возврата не успевает доползти до предсказателя ветвлений. На более старых процессорах может быть примерно аналогичный по размерам проигрыш в этом месте (а когда вы принудительно «вставляете компилятору палки в колёса и не даёте ему заняться оптимизациями, то да, можно и ~25-30% проигрыша получить).

Главная проблема: с какого перепугу вы заменили „указатель“ на „указатель на указатель“? Конечно у вас замедление будет (если функция не слишком мала)!

Господа, попробуйте запустить вот этот код, и проверить «крутость» своих компиляторов ;)
«Крутизна» компилятора никак не может заменить мозги. Но если не загонять компилятор «в угол» и использовать unique_ptr, то результат будет уже таким:
Start test auto!
Result auto t=5899570, r=399999998
Start test new/delete!
Result new/delete t=5768141, r=399999998
-------- Surprize -------
Start test auto (2)!
Result auto (2) t=0, r=399999998
Result new/delete (2) t=0, r=399999998 Fatality!!!!!!
Про отсутствие „сурпрайза“ мы уже говорили, а скорость двух версий — совпадает (в пределах погрешности измерений). Да и с чего им отличаться? В обоих случаях в цикле — 9 инструкций (правда разных):
... Код для unique_ptr ...
.L19:
        movl    $8, %edi
        call    operator new(unsigned long)
        movq    %rax, %rdi
        movl    %ebx, (%rdi)
        movl    %ebx, 4(%rdi)
        movl    $8, %esi
        addl    $1, %ebx
        call    operator delete(void*, unsigned long)
        cmpl    $200000000, %ebx
        jne     .L19

...
... Код для new/delete ...
.L20:
        movl    $8, %edi
        call    operator new(unsigned long)
        movl    $8, %esi
        movl    %ebx, (%rax)
        movl    %ebx, 4(%rax)
        movq    %rax, %rdi
        addl    $1, %ebx
        call    operator delete(void*, unsigned long)
        cmpl    $200000000, %ebx
        jne     .L20


То, что идеальный код, использующий new/delete будет работать быстреe авто-оберток, думаю очевидно.
Я тоже когда-то так думал. Но факты — упрямая вещь. В 2016м году — это дааааалеко не „очевидно“. Современные компиляторы очень, очень хорошо умеют управляться с обёртками. Если ваш код „с обёртками“ и код „без обёрток“ рализуют один и тот же алгоритм — то результат будет, как правило, идентичен. Если вы введёте лишние операции — ну так кто ж вам судья? В коде с unique_ptr передавать сам unique_ptr куда-то не принято: функции, которым не нужно „хранить“ указатель должны получать простой, не „умный“ указатель, а если вы „передаёте“ им владение, то нужно передавать его (используя rvalue-ссылки).

new/delete — не враги, а друзья, которых надо использовать с умом и только если в этом есть реальная выгода.
Кто сказал, что это „враги“? Вовсе нет. Но в современном мире их место — там же, где и место для ассемблерных вставок: иногда, очень редко и с помощью new/delete и с помощью ассемблерных вставок можно существенно ускорить код. Но… редко, очень редко.

Лет 10 назад (когда MSVC 6 был ещё вполне „в ходу“) я и STL'ем старался не пользоваться — больно было смотреть на то, что компилятор в результате порождает. Но те времена прошли…
Вот только не во всякой программе оптимизации кодогенерации могут дать «5-7% каждый год лет 10», а заменять пузырьковую сортировку быстрой компиляторы не умеют. Поэтому GCC и Clang… ломают код. Не, ну правда, "NULL как аргумент memcpy/memmove — это UB, поэтому если указатель засветился в вызове memcpy/memmove, мы выкинем все проверки с ним и весь код обработки NULL, а если вы делали memcpy(NULL, NULL, 0) — мы сломали весь ваш код, но вы сами виноваты" — больше похоже на троллинг, чем на приличную оптимизацию. (Разумеется, в чистом виде memcpy(NULL, NULL, 0) не встречается, но memcpy(p, q, len) без дополнительных проверок, где при нулевом len p или q могут быть нулями, — очень даже.)
NULL как аргумент memcpy/memmove — это UB, поэтому если указатель засветился в вызове memcpy/memmove, мы выкинем все проверки с ним и весь код обработки NULL, а если вы делали memcpy(NULL, NULL, 0) — мы сломали весь ваш код, но вы сами виноваты
Ну всё-таки выкидываются не «все проверки», а «все проверки после вызова memmove». Также выкидываются проверки this и т.д. и т.п. А чего вы хотите? C/C++ — это не Java и не C#…

Кто вообще сказал, что «будет легко»? Я не зря чуть выше писал «С++ — сложный, хитрый, опасный инструмент» — это один из довольно неприятных моментов в нём, да. Программа в которой такое встрчается была сломана с самого начала, так как никто не гарантирует того, что у вас memcpy не читает память до проверки длины. Такие платформы реально существуют, код там примерно такой:
void *memcpy(void *dest, const void *src, size_t n) {
  size_t pre_count = n & 7U;
  n &= ~7U;
  switch (pre_count) {
    do {
      case 0: *dest++ = *src++;
      case 1: *dest++ = *src++;
      case 2: *dest++ = *src++;
      case 3: *dest++ = *src++;
      case 4: *dest++ = *src++;
      case 5: *dest++ = *src++;
      case 6: *dest++ = *src++;
      case 7: *dest++ = *src++;
    } while (n-=8);
  }
}
Про то, что UB — это инструкция для програмиста, а не для компилятора многие присали, в том числе и я, если ваша программа вызывает UB — приготовьтесь к подобным сюрпризам, да. Логика здесь та же самая, которая проверку a + 1 > b + 1 превращает в a > b — но почему-то подобная оптимизация у вас нареканий не вызывает… или вызывает?

Но вообще, если вас этот так волнует (например если вы работает в окружении где NULL — валидный адрес… так встройка иногда сконфигурирована) или где memmove гарантированно не ломается (хотя кто и когда это обещал — сегодня не ломается, завтра сломается), то вы всегда можете использовать опцию -fno-delete-null-pointer-checks.
n=0 — корректное значение длины, а ваша версия memcpy на нём сломается даже при корректных значениях указателей. Попробуйте ещё раз.
Более того, указатель на конец массива — это валидный указатель, поэтому memcpy вообще не имеет права разыменовывать переданные ей указатели при n=0.
Код типа
class MyContainer {
  // если контейнер пуст, то указатель на данные держим нулевым
  size_t Length = 0;
  int* Data = nullptr;
  ...
  void Append(const MyContainer& other) {
    Realloc(Length + other.Length);
    memcpy(Data + Length, other.Data, other.Length * sizeof(int));
    Length += other.Length;
  }
  ...
}
довольно типичен. Он работает на всех платформах, он не был сломан до того, как в gcc закоммитили изменение, его не может сломать корректная реализация memcpy, он не сломан при использовании компилятора, думающего о программистах, типа VC2015.
UB — это не какие-то законы природы, данные нам свыше. UB означает всего лишь «конструкции, которые стандарт пометил словами undefined behaviour». Часть из них действительно отражают то, как устроен компьютерный мир — в неинициализированной переменной может оказаться всё, что угодно, а если она размещена в регистре, то и Itanium-ный Not-a-Thing с исключением при чтении; аналогично при выходе за границы массива. Часть из них отражают законы физики Марса, и джинна выпустили из бутылки, когда фразу «это UB», после которой забыли написать «потому что на Марсе вот так» (signed overflow на системах со странными представлениями отрицательных чисел), начали интерпретировать «поэтому на Земле мы вам делать так тоже запретим» («ну если ваш код вдруг попадёт на Марс, он же уже сломан!»). А часть — просто неудачные формулировки, и интерпретировать фразу со смыслом «memcpy не обязана делать явных проверок своих аргументов на NULL» как «memcpy нельзя передавать NULL даже при копировании нуля байт» — расписка в бессилии сделать что-то приличными средствами.

С99 7.1.4 Use of library functions


If an argument to a function has an invalid value (such as a value outside the domain of the function, or a pointer outside the address space of the program, or a null pointer, or a pointer to non-modifiable storage when the corresponding parameter is not const-qualified) or a type (after promotion) not expected by a function with variable number of arguments, the behavior is undefined.

Я не знаю, как это можно по-другому интерпретировать.

Я же писал: это значит, что memcpy (как и прочие функции) не обязана делать явных проверок своих аргументов. Если попросить memcpy скопировать один байт из одного мусорного указателя в другой, она может упасть (в случае NULL или совсем невалидных указателей) или действительно скопировать один байт, последствия чего в случае некорректного dst-указателя непредсказуемы.
У вас станная интерперетация. Очень странная. Обратите внимание на то, что эта фраза живёт не в разделе «Implementation of functions», а в разделе «Use of library functions» — то есть направлена она на программиста, который эти функции будет вызывать, а не писать.

Но чтобы уж совсем никаких разночтений не осталось в C11 описания фукнкция из <string.h> было дополнено: Where an argument declared as size_t n specifies the length of the array for a function, n can have the value zero on a call to that function. Unless explicitly stated otherwise in the description of a particular function in this subclause, pointer arguments on such a call shall still have valid values, as described in 7.1.4..

Изивините — но разработчики GCC сделали то, что стандарт им явным образом разрешил — и не более того.

P.S. И, кстати, стало действительно видно, что моя реализация — неправильна. Посыпаю голову пеплом. Но то, что моя реализация некорректна не делает вашу программу правильной, увы.

Код уже был сломан. Ничего дополнительно компиляторы не ломали.

x-----x
Наивность — это у вас. У нас — годы практики. Хорошо известно, что MSVC и Borland/Inprise/Embarcadero — генерируют отвратительный код.
x-----x
— Эхх… Зачем же на личности-то переходить? ;) Если у Вас годы практики, то у вашего покорного слуги — десятки. И лет 20 назад, когда я студентом в колледже после лаб по ассемблеру, под досом запускал этот волшебный watcom c++ мы про STL даже и не слышали c интернетом туговато было. Но за-то у нас считалась модной фишка — создать текстовый файл, забить там по памяти строчку символов и после переименовав его в xxx.com выполнить «теплую» перезагрузку дос :)
Мое " Какая наивность…", относится к попытке объявлять некоторыми представителями человечества свое субъективное мнение — общепринятым. Неужели кто-то на полном серьезе протестировал все версии всех компиляторов и сравнил их все между собой на всех возможных программах? У всех компиляторов есть свои плюсы и минусы, но безапелляционно утверждать, что у кого-то отвратительный код, а у кого-то хороший… слишком большая вольность.

Возьмем наш пример с сюрпризом.
Для объективности сравнения, я все-таки скачал последний MinGW и скомпилил в нем код, вообще без каких либо изменений. Извиняюсь, прошлый раз не выложил инклуды (это про GetTickCount)
#include <windows.h>
#include <tchar.h>
#include <stdio.h>
#include <conio.h>
#include Вот результат для auto_ptr vs new/delete с несколькими вариантами оптимизации gcc
g++ test1.cpp
Start test auto!
Result auto t=6266, r=39999998
Start test new/delete!
Result new/delete t=4687, r=39999998
— Surprize -------x
Start test auto (2)!
Result auto (2) t=938, r=39999998
Start test new/delete (2)!
Result new/delete (2) t=359, r=39999998 Fatality!!!

g++ test1.cpp -O1
Start test auto!
Result auto t=5000, r=39999998
Start test new/delete!
Result new/delete t=4531, r=39999998
— Surprize -------x
Start test auto (2)!
Result auto (2) t=234, r=39999998
Start test new/delete (2)!
Result new/delete (2) t=157, r=39999998 Fatality!!!

g++ test1.cpp -O2
Start test auto!
Result auto t=4563, r=39999998
Start test new/delete!
Result new/delete t=3968, r=39999998
— Surprize -------x
Start test auto (2)!
Result auto (2) t=0, r=39999998
Start test new/delete (2)!
Result new/delete (2) t=0, r=39999998 Fatality!!!

Напомню результат с сюрпризом C++Builder XE3 (оптимизация была включена на fasters код):
Start test auto!
Result auto t=5203, r=39999998
Start test new/delete!
Result new/delete t=2282, r=39999998
x-------- Surprize -------x
Start test auto (2)!
Result auto (2) t=2937, r=39999998
Start test new/delete (2)!
Result new/delete (2) t=172, r=39999998 Fatality!!!

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

x-------x
Выводы:
x-------x
auto_ptr Тест с созданием и удалением в цикле:
gсс 01=5000, O2=4563
cb = 5203
Если сравнивать по O1, то примерно одинаково, по O2 — gcc на 12% быстрее
x-------x
new/delete Тест с созданием и удалением в цикле:
gсс 01=4531, O2=3968
cb = 2282
Если сравнивать по O1, то bc в 2раза быстрее, по O2 — bc на 42% быстрее

Ну КАК тут можно утверждать, что в bc — код отвратительный???
И самое главное: и в bc и в gcc код new/delete быстрее кода auto_ptr

Смотрим, что с сюрпризом:
x-------x
auto_ptr Тест только с присваиванием в цикле:
gсс 01=234, O2=0
cb = 2937
Если сравнивать по O1 — то gcc в 12.5 раз быстрее, по O2 — gcc удалил(оптимизировал) весь код
x-------x
new/delete Тест только с присваиванием в цикле:
gсс 01=157, O2=0
cb = 172
Если сравнивать по O1 — то примерно одинаково, по O2 — gcc удалил(оптимизировал) весь код

Вот тут можно сказать — в bc присваивание auto_ptr в цикле выполняется чертовски долго.
Но, опять же, самое главное: и в bc и в gcc код new/delete быстрее кода auto_ptr!!! Хотя в gcc vs gcc сюрприз не такой крутой (67%) как в bc vs bc (17раз)

Общий вывод: new/delete и в bc и в gcc работают быстре чем auto_ptr.

x-------x
Идем дальше, и пробуем поменять auto_ptr на unique_ptr:
x-------x
g++ test2.cpp -std=c++14
Start test unique_ptr!
Result unique_ptr t=9875, r=39999998
Start test new/delete!
Result new/delete t=4782, r=39999998
— Surprize -------x
Start test unique_ptr (2)!
Result unique_ptr (2) t=3000, r=39999998
Start test new/delete (2)!
Result new/delete (2) t=375, r=39999998 Fatality!!!

g++ test2.cpp -std=c++14 -O1
Start test unique_ptr!
Result unique_ptr t=4813, r=39999998
Start test new/delete!
Result new/delete t=4687, r=39999998
— Surprize -------x
Start test unique_ptr (2)!
Result unique_ptr (2) t=250, r=39999998
Start test new/delete (2)!
Result new/delete (2) t=187, r=39999998 Fatality!!!

g++ test2.cpp -std=c++14 -O2
Start test unique_ptr!
Result unique_ptr t=4219, r=39999998
Start test new/delete!
Result new/delete t=4141, r=39999998
— Surprize -------x
Start test unique_ptr (2)!
Result unique_ptr (2) t=0, r=39999998
Start test new/delete (2)!
Result new/delete (2) t=0, r=39999998 Fatality!!!

x-------x
Выводы:
x-------x
Тест повторил несколько раз unique_ptr vs new/dellete = хоть и не намного, но new/dellete стабильно быстрее unique_ptr (в O1 разница более заметна чем в O2)

Ну и последнее, попробуем все-таки сделать компилятору gcc подсечку даже для режима O2 и изменим код так, чтоб он не смог додуматься до оптимизации цикла с сюрпризом в 0тиков и посмотрим, что в реальности кроется за этими нулями:

struct MyStruct
{
volatile int x;
volatile int y;
};

int k = 2000000000; //+00 иначе слишком шустро

— Surprize -------x
Start test unique_ptr (2)!
Result unique_ptr (2) t=7672, r=-294967298
Start test new/delete (2)!
Result new/delete (2) t=7641, r=-294967298

— Surprize -------x
Start test unique_ptr (2)!
Result unique_ptr (2) t=7703, r=-294967298
Start test new/delete (2)!
Result new/delete (2) t=7656, r=-294967298

— Surprize -------x
Start test unique_ptr (2)!
Result unique_ptr (2) t=7641, r=-294967298
Start test new/delete (2)!
Result new/delete (2) t=7656, r=-294967298

Итого: в O2 сюрприз (т.е. только присваивание в цикле) для new/delete и unique_ptr работает одинаково. Для создания/удаления в цикле new/delete работает немного быстрее, но разница незначительна. А вот разница new/delete vs auto_ptr заметна и в gcc и в bc auto_ptr — медленнее.

Очевидно ли, что код с оберткой медленнее, чем без нее?
Можно ли утверждать, что компилятор создаст для обертки код по скорости не уступающий ее ручному развороту?
Пожалуй, ответить можно так:
— Если без оптимизации, да очевидно, код с оберткой будет медленнее.
— Если с оптимизацией, то всё зависит от компилятора и самой программы, насколько хитро организован код и сможет ли его «понять» компилятор, лучший способ узнать — это самостоятельно проверить, что-то заранее утверждать тут действительно не стоит… Доверие к компилятору конечно растет по мере успешности таких проверок, но не стоит свое доверие принимать за «общепринятый» стандарт/качество, слишком сложные критерии для объективной оценки.

Вы меня прямо заинтриговали, я немного поигрался с вашими тестами. Во-первых, оставил только вариант с сюрпризом. Во-вторых, сделал запуски функции идентичными для голого указателя и для unique_ptr. Потому как вы передавали unique_ptr по ссылке, что в данном сценарии не подходит: функция ведь не участвует в определении времени жизни объекта, следовательно, и передавать объект нужно именно по голому указателю, в соответствии с C++ Core Guidelines. Сразу же оговорюсь, что если передавать объект именно как ссылку на умный указатель, результаты не меняются, я проверил.


В-третьих, заменил auto_ptr на unique_ptr. Ну потому что ну в конце-то концов. Наконец, я добавил ещё один вариант с более сложной функцией, далее станет ясно, почему. Протестировал это дело на gcc, clang и VS. Результаты:


gcc 4.9.3:


Simple test with unique_ptr done in 0
Simple test with new/delete done in 0
Complex test with unique_ptr done in 851
Complex test with new/delete done in 850

clang 3.7.0:


Simple test with unique_ptr done in 0
Simple test with new/delete done in 0
Complex test with unique_ptr done in 1057
Complex test with new/delete done in 1046

VS 2015 (количество итераций уменьшено в 4 раза):


Simple test with unique_ptr done in 4021
Simple test with new/delete done in 575
Complex test with unique_ptr done in 4347
Complex test with new/delete done in 1233

Мы видим следующее:


  • gcc и clang соптимизировали простую версию "с сюрпризом" в ноль. Просто догадались, что результат зависит только от последней итерации. Весьма впечатляюще.
  • gcc и clang работают для unique_ptr и new/delete на более сложной функции одинаково.
  • Оптимизатор VS показал себя не очень. Чтобы уложиться в лимит времени онлайн-компиляторов пришлось даже уменьшить ему число итераций в 4 раза.

Что характерно, у меня на машине локально VS показала себя более достойно (возможно, на сервере стоит не та оптимизация, не знаю, не нашёл, где посмотреть ключи). В частности, время выполнения более сложной функции для unique_ptr и new/delete отличается не сильно. Но всё же new/delete выигрывает.


Теперь, мне вообще кажется, что ваш пример не слишком показателен. В цикле из всего функционала умного указателя вы по сути использовали только operator->(). Я в своём примере использовал только get() (но, повторюсь, ваш я тоже проверил). Так вот, и то и другое — однострочные функции, объявленные в заголовочном файле. Такие вещи компиляторы научились инлайнить давным-давно (Visual C++, видимо, это исключение :) ). Я не уверен, что вы сможете придумать такой пример, на котором gcc или clang смогут сделать какую-то оптимизацию с голым указателем T*, но при этом не смогут применить её над методами умного указателя unique_ptr<T>::get() или unique_ptr<T>::operator->().

Пример был сделан с одной целью: показать что код (со включенной оптимизацией) без оберток работает быстрее чем с обертками.
По результатам для компиляторов bc и vc это оказалось именно так.
Для gcc и clang — практически разницы нет, хотя в gcc все же есть небольшой перевес в сторону более быстрого new/delete.

Спасибо за ссылку на интересный инструмент для тестов, теперь можно подвести окончательные итоги:

Было два варианта тестирования:
A) Тест создания/удаления объектов в цикле в раздельных ф-циях. Т.е. в одной ф-ции объект создается, в другой используется, в третей удаляется и все это делается через один общий обычный или умный указатель.

Почему для void test2_new(MyStruct** in_p, int i) — используется указатель на указатель? — чтобы можно было выделить внутри ф-ции память для нового объекта и ассоциировать его с общим указателем. Точно также через указатель на указатель в test2_delete освобождается память под объект на который указывает общий указатель.

Почему для void test2_auto(std::unique_ptr& in_ap, int i) — используется ссылка, дабы проверить в цикле именно скорость работы reset() и ->(), а не ф-ции get(), использование которой для передачи указателя на объект в ф-цию, делает внутреннюю работу ф-ции абсолютно не связанную с «умными» указателями.

Изменения в онлайн тесте:
Оставил int — дабы компилятор использовал туже разрядность что и на сервере
Добавил volatile — дабы компилятор имел в виду, что значения могут меняться без его ведома через аппаратные прерывания или др.потоки.
Убрал из блока измерения времени ф-цию вывода cout (оставленную вами там видимо по ошибке)

http://rextester.com/NSQ67595
VC k = 20000000
Test2 unique_ptr done in 6629
Test2 with new/delete done in 2061

http://rextester.com/GGNQL2697
CLANG k=VC*4
Test2 unique_ptr done in 2291
Test2 with new/delete done in 2290

http://rextester.com/QEABX66753
GCC k=VC*4
Test2 unique_ptr done in 2478
Test2 with new/delete done in 2340

Для теста A:
VC — плохо оптимизирует unique_ptr
CLANG, GCC — хорошо (но CLANG быстрее GCC)
Абсолютное сравнение по скорости — лучше всех CLANG, потом GCC, ну и VC (в 4раза уменьшено кол-во итераций)

Б) Тест в котором указатель обычный/умный, только используется в цикле.
1. Вы повторили, но с лишними изменениями. Как я уже писал выше — использование get() полностью лишает смысла действия производимые с умным указателем внутри ф-ции, Поэтому цитирую «Теперь, мне вообще кажется, что ваш пример не слишком показателен.» становится не показательным именно из-за вашего изменения, т.к. вы вообще убрали умный указатель из ф-ции, оставив лишь для тестирования только get()
2. Ф-ция test_complex() — так же лишена смысла, т.к. мы мерим не скорость выполнения операции %, а именно доступ к членам объекта через умный/обычный указатель. Для того чтобы компилятор не оптимизировал код и вы не получали 0 и было предложено объявить члены MyStruct через volatile (тогда компилятор будет считать, что члены MyStruct могут быть изменены без его ведома и не сможет их оптимизировать).

Результаты локальных тестов я выкладывал в сообщении выше, а вот результаты online:

http://rextester.com/LRD24665
VC k = 80000000
Test3 unique_ptr done in 14452
Test3 with new/delete done in 595

http://rextester.com/JWH56302
CLANG k = VC*4
Test3 unique_ptr done in 267
Test3 with new/delete done in 269

http://rextester.com/MICE52083
GCC k = VC*4
Test3 unique_ptr done in 319
Test3 with new/delete done in 314

Для теста Б:
VC — плохо оптимизирует unique_ptr
CLANG, GCC — хорошо
Абсолютное сравнение по скорости — лучше всех CLANG, потом GCC, ну и VC ( в 4раза уменьшено кол-во итераций)

Ну и вспомним локальное сравнение auto_ptr — new/delete для BC vc GCC

Тест А:
BC
Result auto t=5203, r=39999998
Result new/delete t=2282, r=39999998
GCC -O2
Result auto t=4563, r=39999998
Result new/delete t=3968, r=39999998

BC — плохо оптимизирует auto_ptr
GCC — средне
По абсолютной скорости — BC в 2раза быстре GCC (если использовать new/delete)

Тест Б:
BC
Result auto (2) t=2937, r=39999998
Result new/delete (2) t=172, r=39999998
GCC -O2
Result auto (2) t=63, r=39999998
Result new/delete (2) t=78, r=39999998

BC — плохо оптимизирует auto_ptr
GCC — хорошо
По абсолютной скорости — а вот тут уже GCC в 2раза быстре BC (если использовать new/delete)

x-----x
В общем и целом, конечно удивляет такой провал VC. Но вывод все-таки остается тем же, доверяй, но проверяй. В BC и VC явно оптимизация не настолько крута, и поэтому для ускорения работы замена оберток на new/delete вполне может быть оправдана. Для clang и gcc, это более спорный вопрос. По вашим измененным тестам видно, что get() оптимизируется так же хорошо как reset() и ->() в моих, поэтому наверно сразу с ходу такой пример, который вы предложили привести будет сложно. Да и нужно ли? Самый лучший пример — это реальная программа для реального компилятора используемого в релизах, в которой либо можно существенно увеличить быстродействие за счет new/delete либо нельзя т.к. компилятор это сделал за вас.
В общем и целом, конечно удивляет такой провал VC.

Да, я тоже был немало удивлён и решил повторить ваши тесты у себя локально. Получил совершенно другие результаты. Тест А: и unique_ptr и new/delete дали одинаковое время работы. Тест Б: unique_ptr проиграл new/delete в полтора раза. Похоже, я предложил не самый объективный инструмент для замера скорости, прошу прощения. Я уже воспользовался формой обратной связи на сайте и сообщил о проблеме.


Но основной ваш вывод, остаётся в силе. Тест Б всё равно работает в полтора раза медленнее для умных указателей. Я зарепортил баг на Microsoft.Connect (но его пока не видно в системе; возможно, он на премодерации, а может у них просто всё лагает).

Честно говоря меня удивляет это удивление (не знаю уж до какого уровня рефлексии можно тут дойти).

Да, если считать верхом совершенства, прости господи, watcom c++, то это может оказаться удивительным, так как в прошлом веке Borland C++ действительно ещё был «на коне», да и MSVC был неплох, да. Но с тех пор — много воды утекло. Как я уже писал: в прошлом веке я и STL'ем старался не пользоваться — больно было смотреть на то, что компилятор в результате порождает. Но те времена прошли…

Я с большим уважением отношусь к «старому» Borland'у. Borland C++ 3.1 — это вообще чудо было. Напомню что хотя Borland С++ был «базовым компилятором» для разработчиков STL (потому что остальные компиляторы тогда просто не могли все эти шаблоны разобрать вообще) — но ни о какой скорости тогда речь не шла, была задача «тупо это всё собрать хоть как-то и не упасть».

Но к 1996м году Microsoft тупо «задавил» Borland (и все остальные независимые компании: Watcom/Sybase, Zortech/Symantec, JPI/TopSpeed и прочие всякие Comeau) — мало того, что денег на разработку стало не хватать, так ещё и, в случае с Borland'ом (последний «живой» конкурент) существенную часть команды разработчиков в 1996м году сманили — но они занялись не C++, а совсем другим: считалось, что C++ «скоро уйдёт в историю» и все-все-все будут использовать managed код, который «догонит и перегонит» старичка.

Чего, разумеется, не случилось. Развитие C++ продолжилось — но так как хорошие деньги именно за скорость платили только в области HPC, то приличные компиляторы только там и остались: Intel C++, IBM XLC++, Sun/ Oracle C++ (хотя этот, в последнее время, сдал), потом GCC подтянули (примерно к середине нулевых). Напомню, что Windows на HPC никто не использует: в пресловутом Top500 ни одной Windows-системы сейчас нет, да и «в лучшие времена» их там было в районе 1% — да и, собственно, если вам уж сильно была нужна скорость — всегда можно было использовать Intel C++ (он встраивается в Visual Studio).

А дальше… дальше Apple крупно разосрался со Столлманом (после выхода в 2005м году GPLv3) и вбухал кучу денег в свой собственный компилятор, который, однако Apple не стал «держать при себе», а наоборот, всячески пропогандировал разработку «всем миром» (так что сейчас Apple уже не является даже основным контрибутором). Microsoft это слабо волновало до тех пор пока «всем миром» clang не допилили до состояния почти совместимости с MSVC (сам Apple этим не занимался, но видимо кому-то это сильно нужно было). Chromium, скажем, собрать только с помощью clang'а нельзя — но только за счёт того, что кой-какие расширения не поддерживаются, 99% кода clang собирает и это используется чтобы использовать ASAN.

В какой-то момент Microsoft понял, что стратегия «заставим всех мышей жрать кактус за неимением другой альтернативы» и «сменил пластинку». Балмера турнули, разработчики MSVC снова получили деньги под развитие C++ (думаю не стоит объяснять что замена одного человека круто развернуть компанию размера Microsoft'а не может — увольнение Баллмера было следствием, а не причиной изменения курса). Примерно с MSVC 2013 о компиляторе MSVC уже можно говорить как о «современном компиляторе C++» (то, что было до этого тянуло в лучшем случае на «муляж компилятора C++»), начиная с MSVC 2015 — его уже можно даже с другими сравнивать (хотя он и проигрывает частенько, но, в общем — движение в нужную сторону очень и очень заметно).

Всё вышеописанное — банальности. Как можно считать себя знатоком C++ и всего этого не знать — я просто не понимаю.

Я ведь когда написал «хорошо известно, что MSVC и Borland/Inprise/Embarcadero — генерируют отвратительный код» — то я ведь не издевался. Как мне казалось я просто говорил вещь, которая всем очевидна, известна и которую все учитывают при разработке — а меня вдруг обвинили в «переходе на личности».

P.S. Что касается сравнени «абсолютных скоростей new/delete» — то тут я просто не хочу затевать дискуссию. Боюсь на мат перейти. Просто хочу напомнить что скорость работы, собственно, new/delete от компилятора зависит мало (основное время проводится в malloc'е), стандартные реализации malloc'а в HPC не используются (популярны вещи типа jemalloc/TCMalloc), да и MingW разработчиков GCC мало волнует вообще (ну не используюется Windows людьми, для которых скорость работы C++ кода превращается в живые деньги — все эти Facebook'и, Google'ы и Yandex'ы используют Linux или, иногда, FreeBSD). Зачем вообще его сюда приплели — мне не очень ясно: как я уже <a href="">говорил — если вам нужен приличный компилятор под Windows — то нужно использовать clang: он пока чуть-чуть отстаёт от GCC, но зато вы всегда можете всегда использовать его только для «тяжёлого» кода, оставив тот же runtime и использовав MSVC в тех местах, где clang пока не тянет.
Продолжу рефлексию и удивлюсь вашему удивлению…
С++ это один из рабочих инструментов, который используется в работе над проектами, какой компилятор стоит у работодателя, такой и используется. Да я не эксперт в области истории развития компиляторов, и могу судить только о тех, с которыми реально сталкивался и имею опыт «боевого» использования в коммерческих продуктах (например первые BC и еще тогда MS Torbo C в свое время активно использовали для промышленных контроллеров с ОС ROM-DOS, VC6 — для мелких прикладных задач c GUI, очень плевался от него, когда перешли на BC6 это стало с точки зрения RAD разработки по сравнению с VC6 небо и земля, скорость реализации проектов ускорилась в разы, хотя по тестам производительности, которые тогда делали BC6 где-то проигрывал VC6 15-20% где-то выигрывал, но для прикладных задач это вообще ни о чем). С gcc когда-то попробовал один раз поиграться и забросил, так как никакой практической пользы в его использовании для себя не увидел…

И мое сугубо личное мнение, которое сложилось по опыту работы — у каждого компилятора, есть свои плюсы/минусы и нельзя так просто считать некий компилятор генерящим отвратительный код, а другой генерящим нормальный. Потому что код который он генерирует зависит от того, что написано в программе и насколько хорошо именно эту программу «понимает» компилятор.
Собственно это странное мнение, что профессионалом может быть только тот, кто имел опыт работы со всеми компиляторами меня и удивляет. Но а если вы про «специалиста по всем компиляторам c++», то да таковым я не являюсь, поэтому некоторые вещи меня в этом мире могут удивить :)

Про «абсолютных скоростей new/delete» скажу кратко, то, что new использует malloc знают все (а как реализовывать malloc это дело уже компилятора). Только вот для одной и той же программы использующей new/delete на одном компиляторе получается бинарник работающий быстрее чем на другом — и это просто факт (по крайней мере для Windows).
Собственно это странное мнение, что профессионалом может быть только тот, кто имел опыт работы со всеми компиляторами меня и удивляет.
Причём тут профессионализм?

Давайтё я проведу «бытовую аналогию»: будет ли таксист отлично занющий где какие камеры висят и в каких местах можно «нарваться на ментов», но при этом не знающий какие есть автопроизводители в мире и чем отличаются выпускаемые ими машины считать «профессионалом»? Да, наверно. Но когда подобный «профессионал» начинает рассуждать о том как тюнить машину для раллийных гонок — то он выглядит несколько глупо.

И мое сугубо личное мнение, которое сложилось по опыту работы — у каждого компилятора, есть свои плюсы/минусы и нельзя так просто считать некий компилятор генерящим отвратительный код, а другой генерящим нормальный.
Свои плюсы и минусы есть, несомненно, у всех компиляторов. Скажем у GCC кодогенерация под x86 — лучше, чем у clang'а, а вот его же версия для arm'а — уже далеко не так хорошо себя ведёт. А есть компиляторы, которых «пугают» даже inline функции (на которых реально можно получить выигрыш заменой inline-функции на макрос) в достаточно несложных ситуациях (к ним относятся в усновном устаревшие поделки типа Borland/Inprise/Embarcadero, Open Watcom'а и прочих — но также и MSVC).

Однако если про устаревшие компиляторы забыть, то споры об «обёртках» сегодня имеют столько же смысла, сколько споры о том, что лучше использовать — указатель или индекс (да, представьте себе, лет 20 назад подобные споры реально имели место было — потому что, удивительно, но факт: тогдашним компиляторам это не было не всё равно): все качественные современные компиляторы уже давно этим «переболели» и однострочные обёртки типа unqiue_ptr — код не замедляют. Отсюда — и обсуждаемая статья.

А ваши рассуждения об обёртках потому и вызвали столько негатива, что выглядят они… — как у Жванецкого: «давайте спорить о вкусе устриц и кокосовых орехов с теми, кто их ел».

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

хотя по тестам производительности, которые тогда делали BC6 где-то проигрывал VC6 15-20% где-то выигрывал, но для прикладных задач это вообще ни о чем
Ну если для вас 15-20% — это «ни о чём», то, может быть, предоставите право судить о скорости тем, для кого 15-20% — это важно? И кто, соотвественно, следит за этим и знает откуда в современном мире тормоза вылезают часто, а откуда — редко?

С++ это один из рабочих инструментов, который используется в работе над проектами, какой компилятор стоит у работодателя, такой и используется.
Такое тоже, бывает, да. Но тут, как бы, «если слаще морковки ничего не пробовал,… то и тыква — фрукт». У нас в проекте есть куча костылей для MSVC, от которого мы не можем, по некоторым причинам, пока отказаться. И выкинуть их не удаётся годами (говорят что начиная с MSVC 15 «всё будет по другому»… посмотрим — на MSVC 15 мы только-только перешли). А вот костылей для GCC и Clang'а — очень мало — и большинство из них существует только год-два пока переход на новую версию не сделает их ненужными. В частности для разработчиков GCC/Clang'а плохо «свернувшаяся» обёртка — это ошибка, которую нужно править, для разработчиков MSVC — ну я не знаю, сейчас, может ситация изменилась, но лет пять назад ничего, кроме «используйте __forceinline» поддержка сказать не могла (хотя и это тольком не помогало, полностью «свернуть» обёртки MSVC всё равно не мог).

Только вот для одной и той же программы использующей new/delete на одном компиляторе получается бинарник работающий быстрее чем на другом — и это просто факт (по крайней мере для Windows).
А вот тут — у нас уже рассуждения «суперпрофитаксиста», который баранку вертеть умеет, а том, что под капотом у машины — не знает и знать не хочет. А вы не поверите — там бывает не только двигатель, но и трансмиссия и даже, о ужас, коробка передач. И бывает так, что неплохая, в целом, машина проигрывает в ралли из-за неудачного механизма оной коробки передач.

А как реализовывать malloc это дело уже компилятора
В том-то и дело, что нет. Windows — это единственная современная операционка, где malloc не поставляется «из коробки». На всех остальных операционках runtime (включающий, конечно, и malloc) — поставляется с системой. Потому ни GCC, ни Clang mallocа просто не имеют. Microsoft, кстати, тоже до этого наконец-то дотопал, но пока MingW этот runtime не использует (потому что, в частности, «из коробки» он доступен только в Windows 10, а пользователи ещё не все на Windows 10 перешли).

Я понятия не имеют что взяли разработчики MingW, чтобы создать нечто, что можно запускать на Windows — скорее всего какой-нибудь древний dmalloc. Неудивительно, что он отстаёт от mallocа C++Builder'а — всё-таки C++Builder разрабатывается не кучкой энтузиастов для запуска пары утилит во время разработки, а довольно большой компанией, которая когда-то имела приличную команду разработчиков…
Я думаю, что раллийный «профессионал» будет выглядеть не менее глупо, если начнет искать двигатель у запорожца спереди.

x-----x
Ну если для вас 15-20% — это «ни о чём», то, может быть, предоставите право судить о скорости тем, для кого 15-20% — это важно?
x-----x
— Предоставляю! :) Если вам очень важно, что окно с сообщением «Вы нажали не на ту кнопку!», появится на 1милисекунду позже, чем на другом компиляторе — то это ваш личный фетиш. Вы читайте внимательнее к чему относятся эти 15-20%: «для мелких прикладных задач c GUI». Для задач критичных по времени, (а на МК все такие), идет битва за каждую микросекунду и поверьте, тут уж есть, что потюнинговать, чтобы сократить время рабочего цикла, это вот как раз и есть настоящие «ралли». Причем понятиям «ошибка», «зависание»,«утечка» здесь придается не «философский» смысл — типа перезагрузил программу / ОС, и все можно жить с этим дальше, а довольно «фатальный», не закрылся вовремя клапан / не выключился движек… и все… что-то громко бабахнуло. А избежать этого помогают, только беспощадные тесты на производительность и надежность.
x-----x
«если слаще морковки ничего не пробовал,… то и тыква — фрукт».
x-----x
— Те МК, которыми мы занимаемся уже идут со встроенной ОС, поэтому выбирать тут особо не приходится. А инструменты и библиотеки под них, которые разрабатывались, отлаживались и проверялись годами никто менять на «нечто из огромного выбора», конечно не собирается.
Если инструмент проверенный, работает быстро и надежно — вы правда считаете, что его надо поменять?

x-----x
А вот тут — у нас уже рассуждения «суперпрофитаксиста», который баранку вертеть умеет, а том, что под капотом у машины — не знает и знать не хочет.
В том-то и дело, что нет. Windows — это единственная современная операционка, где malloc не поставляется «из коробки».
x-----x
— У вас, какой-то прям «дар» невнимательности — я специально написал (по крайней мере для Windows), т.е. именно «кривая» реализация malloc/free и является проблемой new/delete GCC в Win, и это был ответ на ваше более раннее утверждение: «Просто хочу напомнить что скорость работы, собственно, new/delete от компилятора зависит мало». Поверьте, то что происходит под капотом винды, меня весьма интересует.

Думаю на этом можно завершить данную дискуссию. Подведем итоги:

Мое утверждение:
То, что идеальный код, использующий new/delete будет работать быстреe авто-оберток, думаю очевидно.
— Неверно, ибо есть компиляторы, для которых это действительно очевидно, и есть компиляторы, для которых это не настолько очевидно, т.к. используется сильная оптимизация.

Утверждение GamePad64:
Тот же unique_ptr соберётся в точно такой же машинный код, что и new/delete.
— Неверно, т.к есть компиляторы в которых это по тестам, действительно так (по крайней мере по тем конкретным тестам, которые мы тут проводили). И есть компиляторы в которых это совершенно не так (доказано теми же тестами)

Утверждение khim
Хорошо известно, что MSVC и Borland/Inprise/Embarcadero — генерируют отвратительный код.
— Не выдерживает критики, т.к. здесь приведены тесты в которых код скомпилированный GCC выполняется в 2раза дольше кода BC (на Windows).

Остальные утверждения в частности:
То, что идеальный код, использующий new/delete написать сложно, тоже очевидно.
Поэтому в 99% случаев авто-обертки использовать гораздо разумнее.

Вроде бы никто не оспаривал.

Главный вывод — на данный момент, любое утверждение о компиляторах с++, должно содержать в себе перечисление тех компиляторов к которым оно относится. И если это утверждение пытается как-то сравнивать компиляторы между собой, необходимо приводить критерии и тесты на которых оно основано. Так как любой частный случай/тест не вписывающийся в общую картину — опровергает всё обобщенное утверждение.

Засим и откланиваюсь.

а через пару лет, когда все компиляторы научатся генерировать для оберток столь же эффективный код, что и для new/delete, вы будете заставлять джуниоров переписывать new/delete на shared_ptr/unique_ptr?
Хватит уже. Вы спорите с человеком, который на одном компиляторе запускает «пузырьковую сортировку», а на другом «сортировку слиянием» и делает из этого какие-то выводы о качестве компиляторов.

Это либо профнепригодность, либо троллинг — в любом случае обсуждать нечего.
По моему, главный тролль в вашем лице как раз и обозначился.
Все тесты были приведены выше, они запускались на всех компиляторах без изменений и проверить это может любой.
Код, который был приведен выше — был абсолютно тривиальный и без включенной оптимизации
О, а я был прав. В этом примере поди снова не включили?
Мое мнение, что история про «радиоактивный» new высосана из пальца и не нужно быть семи пядей во лбу, чтобы понять кто и зачем это сделал.
Смотрим на WinRT API, где вообще на каждый чих и пук у вас появляется новый shared_ptr (в виде ^) и очевидно, что Microsoft, было очень соблазнительно приучить всех пользовать make_shared, дабы сократить количество аллокаций из кучи в практически любой WinRT программе вдвое. Игра стоит свеч.

Вне WinRT мирочка картина получается несколько иная.

#1. Реклама про thread-safe безопасность make_xxx функций не более, чем реклама. На практике очень редко приходится писать выражения, в которых несколько new могут реально породить проблему с исключениями.

#2. В типичной программе на C++ на несколько десятков unique_ptr или scoped_ptr едва приходится один shared_ptr. Соответственно, главного профита от записи make_xxx в виде одного выделения из кучи вы, в подавляющем большинстве случаев использования умных указателей, не получаете.
Если в вашей программе доля shared_ptr сильно больше озвученной — это серьезный повод задуматься, вы явно что-то делаете не так.

#3. В типичной программе подавляющее большинство указателей типа unique_ptr/scoped_ptr это поля класса, а не стековые объекты. Соответственно, даже крошечного бонуса от возможности использовать auto вы тоже не получите. Более того, в списке инициализации или просто в методе, сильно проще писать «new T», чем громоздкое «std::make_unique».

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

зы. Хотя, если думать, что вся эта история в целом подтолкнет хотя бы некоторое количество ортодоксов и консерваторов (коих, к сожалению, все еще есть огромное количество) просто к использованию умных указателей, то это, несомненно, будет большая польза… Правда, мое мнение, что лучше весь этот сброд «C/C++ професионалов», которые типа знали С и выучили C++ через два новых для себя слова — class и virtual, согнать в зоопарки и показывать там на потеху публике.
О чем вы вообще говорите, если Microsoft до недавнего времени в поддержки новых стандартов C++ была в отстающих? Сейчас уже намного лучше, но все равно реализовано не все.

Версия с поддержкой shared_ptr, если я ничего не путаю, появилась в GCC версии 4.0, а это 2005 год. WinRT — это что-то около 2011 года. Связь где?

#1. Реклама про thread-safe безопасность make_xxx функций не более, чем реклама. На практике очень редко приходится писать выражения, в которых несколько new могут реально породить проблему с исключениями.

foo(std::make_shared<T1>(), std::make_shared<T2>());


#2. В типичной программе на C++ на несколько десятков unique_ptr или scoped_ptr едва приходится один shared_ptr. Соответственно, главного профита от записи make_xxx в виде одного выделения из кучи вы, в подавляющем большинстве случаев использования умных указателей, не получаете.
Если в вашей программе доля shared_ptr сильно больше озвученной — это серьезный повод задуматься, вы явно что-то делаете не так.

Ну вот чтобы далеко не лезть — что делают не так авторы OSG из статьи выше (у них своя реализация shared_ptr)? И как вы предлагаете взамен строить сцену из unique_ptr?

#3. В типичной программе подавляющее большинство указателей типа unique_ptr/scoped_ptr это поля класса, а не стековые объекты.

unique_ptr да, а что насчет shared_ptr?

Более того, в списке инициализации или просто в методе, сильно проще писать «new T», чем громоздкое «std::make_unique».

А namespace никто не использует, да?

В «зы» вы вообще какие-то неадекватные причинно-следственные связи делаете. Умные указатели — это хорошо, но использовать их будут те, кто пришел из мира С, а они по умолчанию тупые и недостойные зваться профессионалами, потому что выучили только class и virtual, хоть и пользуются умными указателями? Что-то каша какая-то у вас.

shared_ptr — это просто удобный инструмент для работы с долгоживущими объектами с общим доступом. Нам не нужно следить за его удалением, аккуратной передачей и прочими бестолковыми вещами. В 99% это НИКАК не влияет на производительность и потребление памяти конечного продукта. Если это удобно и эффективно, то это нужно использовать.
как раз таки с поддержкой времени жизни долгоживущих объектов проблем нет — их можно либо через raii создавать/удалять, либо (самый простой вариант) выделять на стеке в main. В самых неудобных случаях делаются синглтоны/мультитоны. А вот всякие нетривиальные, «недолгоживущие» и всем потребные сущности без shared_ptr тяжеловато мониторить.
Ну, под долгоживущими объектами я это и имел ввиду. Мы что-то создали, обработали, в некоторый неопределенным момент времени удалили и т.д.
WinRT — это что-то около 2011 года. Связь где?


#1. Не надо путать shared_ptr и make_shared. Мой комментарий был именно о последнем в контексте «радиоактивности» new.
#2. GCC тут вообще не при чем, изначально shared_ptr это зверь из boost.

foo(std::make_shared<T1>(), std::make_shared<T2>());



Если мы говорим о старом коде, до make_shared, то ваш пример должен выглядеть примерно так
foo( std::shared_ptr<Type1>( new Type1( /* arguments for Type1 */) ), std::shared_ptr<Type2>( new Type2( /* arguments for Type2 */) ) );


Не знаю, кто как, но люди, которые моют руки перед едой, так код писать точно не будут. А напишут его, например, так:
std::shared_ptr<Type1> arg0( new Type1( /* arguments for Type1 */) );
std::shared_ptr<Type2> arg1( new Type2( /* arguments for Type2 */) )
foo(arg0, arg1);


Ой! А проблема то с безопасностью исключений как-то сама собой ушла!

что делают не так авторы OSG из статьи выше


Без понятия. А что, именно библиотека OSG является классическим усреднением для некоего типичного C++ кода? С каких пор?
Кстати, предлагаемый костыль make_ref ничего хорошего из себя не представляет, так как лишается главной силы make_shared — возможности экономить на аллокации памяти из кучи.

а что насчет shared_ptr?


Насчет make_shared я сказал — это полезный трюк, но это автоматически не означает, что new это вселенское зло и надо лепить уродливые костыли типа make_unique или make_ref. Никакой связи.

А namespace никто не использует, да?


new T() vs make_unique<T>()


Что я еще должен использовать? Может сделать двухбуквенный using, чтобы эта конструкция выглядела не так уродливо??

Что-то каша какая-то у вас


Каша у тех, кто невнимательно читает чужие комментарии.
Мой комментарий был о том, что можно и нужно смело использовать new и не комплексовать по этому поводу.
А вот тех, кто думает, что пишет на C++, и при этом не используется умные указатели нужно гнать из профессии ссаными тряпками.

shared_ptr — это просто удобный инструмент для работы с долгоживущими объектами


Не надо мне продавать shared_ptr я знаю что это и зачем. Я говорю о том, что если вместо него можно использовать unique_ptr это сильно лучше.
std::shared_ptr<Type1> arg0( new Type1( /* arguments for Type1 */) );
std::shared_ptr<Type2> arg1( new Type2( /* arguments for Type2 */) )
foo(arg0, arg1);

И положили переменные arg0 и arg1, быть может, в ту область видимости, где их быть не должно.

Ничего, кстати, некрасивого тут нет, просто пользуйтесь отступами и выравниванием:
foo( std::shared_ptr<Type1>( new Type1( /* arguments for Type1 */) ), 
       std::shared_ptr<Type2>( new Type2( /* arguments for Type2 */) ) 
);


Без понятия. А что, именно библиотека OSG является классическим усреднением для некоего типичного C++ кода? С каких пор?

А местами не очень понимаю, против какой именно философии вы выступаете.
Если речь о том, что лучше использовать unique_ptr, то вот пример — графический движок + граф сцены. Сюда можно поставить и другой графические движок, там плюс-минус все похоже.

Насчет make_shared я сказал — это полезный трюк, но это автоматически не означает, что new это вселенское зло и надо лепить уродливые костыли типа make_unique или make_ref. Никакой связи.

Конечно, не означает. Но уродливые костыли (кому как, кстати) позволяют избавиться от ненужных аллокаций в клиентском коде.

Каша у тех, кто невнимательно читает чужие комментарии.
Мой комментарий был о том, что можно и нужно смело использовать new и не комплексовать по этому поводу.

Тут я опять же не понимаю, против какой вы философии. Если вы говорите про то, что нужно использовать new T() вместо make_shared(), то, наверное, большинство проектов от этого и правда не умрут (если одна лишняя аллокация им не страшна, что скорее всего).
Если вы говорите, что нужно оперировать везде указателями как тру программист, то я категорически не согласен. При текущем положении дел это не приносит большинству проектов каких-то существенным бонусов, кроме геморроя. Это не всегда так, но в подавляющем большинстве.

Не надо мне продавать shared_ptr я знаю что это и зачем. Я говорю о том, что если вместо него можно использовать unique_ptr это сильно лучше.

Да конечно лучше, кто же спорит-то?! Нужно использовать. Но это далеко не всегда возможно (опять же, графические движки).
Еще раз о моей позиции, она очень проста.
Я против цирка под названием «new есть зло вселенского масштаба», когда на самом деле надо всего лишь сказать, что иногда make_shared может принести ощутимую пользу вашему приложению.
Не знаю, кто как, но люди, которые моют руки перед едой, так код писать точно не будут. А напишут его, например, так:

Только лучше так:
foo(std::move(arg0), std::move(arg1));
Экономим на лишнем инкременте/декременте счетчика и отвязываем время жизни объектов от времени жизни arg0 и arg1.
В целом, согласен, но могу дополнить по части с Qt.

Как вы правильно заметили в mailing lists, есть 3 основных варианта владения памятью в Qt:
1. Хранение в стеке (тривиально)
2. Отсутствие родителя (предполагается new и delete в пользовательском коде)
3. Есть родитель (только new)

Во втором случае мы сами управляем памятью, поэтому можно использовать std::unique_ptr:

auto mainWindow = std::make_unique();

По возможности надо размещать на стеке, но если не получается, то лучше так.
В третьем случае родитель управляет памятью, но концепцию «умных указателей» можно применить и тут:

template
using unowned_ptr = T*;

template
unowned_ptr MakeQObject(QObject& parent, Args&&… args);

template
unowned_ptr MakeQObject(QWidget& parent, Args&&… args);

То есть опять-таки, мы можем обращаться с этим unowned_ptr, как с unique_ptr, и QObject корректно удалится.
Конечно, это немного текучая абстракция, так как мы всё равно должны убедиться, что не удалим родителя раньше времени. Но хоть что-то.

Чем unowned_ptr лучше «сырого» указателя? Тем, что у него информация о владении закреплена в типе. Никому в голову не придёт его удалить. Можно сделать это ошибкой, если вместо using создать специальный класс, но проще не заморачиваться на этот счёт.

Bjarne уже несколько лет толкает идею unowned_ptr в C++. Прямо сейчас он, наряду с другими полезными мелочами, лежит в Guidelines Support Library.

На практике, есть ещё 4 случай, когда объект «подцепляют» к родителю уже после его создания, но при наличии необходимых helpers, 3 вариант будет удобнее и безопаснее.
А зачем? QObject'ы Qt в общем случае отличаются только тем, что удалять их правильнее через deleteLater. Ну так какие проблемы передать этот deleteLater прямо в QSharedPointer?
QSharedPointer<MyObject> obj =
    QSharedPointer<MyObject>(new MyObject, &QObject::deleteLater);
Забыл я про deleteLater, тогда действительно потребуется QSharedPointer, std::unique_ptr уже не спасёт. Тем не менее, можно реализовать функции-обёртки MakeQObject и MakeQChild так, чтобы писать код вроде:

auto obj1 = MakeQObject<MyObject>();

auto obj2 = MakeQChild<MyObject>(parent);

Собственно, один из вопросов, поднятых в статье, — можно ли все выделения памяти в куче записывать в виде:

auto obj = make<MyObject>(arguments);

Где make — подходящая функция создания умного указателя. Ответ — да, можно, причём такой код будет безопасным и унифицированным.
Такие выделения памяти также отлично сочетаются с рекомендациями Herb Sutter.
только не надо именовать функции с большой буквы, пожалуйста… )
Что до deleteLater — его можно и в стандартные смартпоинтеры сунуть. Через лямбду в deleter'е
Во втором случае мы сами управляем памятью, поэтому можно использовать std::unique_ptr:

Согласен, выглядит разумно.


Чем unowned_ptr лучше «сырого» указателя? Тем, что у него информация о владении закреплена в типе. Никому в голову не придёт его удалить.
Bjarne уже несколько лет толкает идею unowned_ptr в C++. Прямо сейчас он, наряду с другими полезными мелочами, лежит в Guidelines Support Library.

Тут вы немного перепутали. В C++ Core Guidelines и в GSL реализован обратный вариант — владеющий указатель owner<T>. Сырой указатель T* считается невладеющим. Об этом Саттер и Страуструп как раз и рассказывали в своих выступлениях (кажется, на GoingNative 2015, могу поискать, если нужно).


По поводу MakeQObject. Там фундаментальная проблема в том, что в Qt, как я понимаю, есть мода разделять создание объектов и их прицепление к родителю. Т.к. исключений там нет, это считается безопасным. Насколько такая аргументация убедительно — сказать не могу, опыт работы с Qt у меня небольшой.


В любом случае, я всячески приветствую предложить ваши идеи в тот же самый список рассылки :)

Каюсь, перепутал.
Про проблему, можно рассмотреть 2 случая:

  1. Объект почти сразу же присоединяется к родителю. Можно переписать через третий вариант, у наследников QObject обычно последним параметром можно передать родителя.
  2. Объект проделывает долгий путь до прикрепления к родителю. Вначале создаём через make_unique (или через QSharedPointer, как подсказывают), потом отбираем у unique_ptr владение и сразу же прикрепляем к родителю. Этот момент можно вынести в отдельную функцию.

Если я всё правильно понимаю, проблема решается в рамках всё тех же make-функций. Я не думаю, что второй случай очень частый, но опыт работы с Qt у меня тоже небольшой. Но идею в список рассылки попробую закинуть :)
не совсем. Иногда у родителя есть несколько способов владения потомком, в зависимости от того, какого формата отображения мы хотим добиться. Я, например, не видел, чтобы виджеты создавались и передавались куда-то настолько далеко, чтобы оборачивание в смартпоинтер имело смысл
Sign up to leave a comment.