Комментарии 37
В не-ARC среде (и в бородатые времена) класс бы выглядел так:
@interface PrinterImpl: NSObject
// объявлять ivar уже не нужно
@property (retain) NSString *message;
- (instancetype)initWithMessage:(NSString *)message;
- (void)print;
@end
@implementation PrinterImpl
// synthesize уже не нужен,
// и вроде как он сгенерирует ivar "_message", а не "message"
- (void)dealloc {
[_message release];
[super dealloc];
}
- (instancetype)initWithMessage:(NSString *)message {
self = [super init];
if (self) {
// запись в ivar, чтобы избежать возможных сайд-эффектов
// когда перегружен setMessage
_message = [message retain];
}
return self;
}
А разве существуют какие-либо проблемы с использованием Objective-C++ совместно с C++ кодом? Насколько мне известно, таких проблем никогда не было.
Новый день, новая статья на тему iOS/macos разработки, вызывающая вопросы.
Называется "Используем Objective-C в C++ без проблем". Хотя, по моим ощущениям, у автора есть пробелы и в одном, и в другом.
Всё началось с Принтера:
class Printer {
public:
Printer(const std::string&);
void Print() const;
};
На этом этапе я уже начал подозревать, что С++ сейчас будет такой себе. Я тоже далеко не эксперт, но некоторые вещи вроде понимаю.
Лирическое отступление о C++.
Как-то раз я посмотрел хороший доклад про этот язык, но к сожалению напрочь забыл кто (кажется, Андрей Александреску) и что рассказывал. Но там была одна хорошая мысль, которую я надолго запомнил.
Мысль о том, что глядя на сигнатуру функции в С++ часто можно сразу представить себе реализацию, можно понять что функция делает. И говоря "сигнатура" имеется в виду именно список агрументов и возвращаемое значение, на название можно даже не смотреть.
Примеры:
// функция только читает из x
void f(const T& x);
// функция записывает в x по ссылке.
// не _может_, а именно так или иначе _записывает_ в x.
// иначе какого чёрта это не const ссылка?
void f(T& x);
// функция использует копию x
void f(T x);
// функция хочет "поглотить" x, например сохранить куда-то.
// можно сделать move
void f(T&& x)
Так вот. Глядя на интерфейс Printer
, можно сразу догадаться, что наверное Print()
будет печатать то, что было передано в конструкторе. Но конструктор говорит о том, что он собирается только читать из аргумента. Хотя на самом деле, он хочет взять аргумент и положить себе в поле. В итоге произойдёт копирование, которого можно было избежать. Но дело даже не в копировании, а в запутывающем интерфейсе. Он говорит "я буду только читать", а на деле происходит что-то другое.
Одна лишь эта "мелочь" уже подрывает моё доверие к статье о C++.
Потом пошёл ObjectiveC.
template<typename T>
auto unique_void(T * ptr) -> unique_void_ptr{
return unique_void_ptr(ptr, [](void const * data) {
[(id)data dealloc];
});
}
Здесь я тоже склонен заявить, что автор совершенно не разбирается в управлении памятью в ObjectiveC.
unique_void функция создаст новый указатель и позаботится о том чтобы память потом почистилась.
Каким образом почистилась? Если подразумевается вызов free()
на указателе на ObjectiveC объект, то я нахожу это плохой идеей. Жизненный цикл ObjectiveC объектов управляется счётчиком ссылок. Что будет, если указателем на PrinterImpl
владеет кто-то ещё? unique_void()
просто уничтожит этот принтер, а другие пользователи получат мусор, хотя имели сильную ссылку. Более того, unique_void
счётчика ссылок не увеличивает, но умеет "убивать". Что если бы это была среда с автоматическим подсчётом ссылок? Тогда PrinterImpl
сразу умер бы, так как никто не держит сильной ссылки на него, и позже этот умный указатель попытался бы освободить мусор. Потом этот прямой вызов dealloc
. Почему было просто не сделать retain
перед созданием unique_ptr
, а вместо dealloc
не вызывать release
? Тогда и память точно освободится правильно (может там далеко не только free()
происходит), и заодно она не освободится тогда, когда этого делать не стоит (когда счётчик ещё не дошёл до нуля).
По моим ощущениям, тут скрестились две неграмотности, и велика вероятность получить неграмотность в квадрате как результат.
ps.
затрагивать неизвестные для меня области программирования
unique_ptr
здесь даже не вызовет никакое free()
, а только вызовет dealloc
, что в свою очередь не приводит ни как каким освобождениям памяти. По всем признакам вместо dealloc
должен был быть release
, плюс заблаговременный retain
.
Хотя про dealloc
я тоже слегка наврал. Я всегда относился к нему как скорее к коллбеку перед тем как система сделает free()
.
Однако, я провёл такой эксперимент:
id x = [NSObject new];
[x dealloc];
[x dealloc]; // EXC_BAD_ACCESS (code=1, address=0xbeaddead4060)
Из чего можно сделать вывод, что память всё же освобождается. Я поставил брейкпоинт на -[NSObject dealloc]
, и единственной инструкцией в реализации является прыжок на _objc_rootDealloc
, в теле которой и находится free()
.
Но как бы там ни было, напрямую я бы всё равно dealloc
не вызывал, потому что на этот объект могут быть сильные ссылки, которым станет плохо.
Спасибо за развернутый комментарий! На счет С++, я с вами согласен, но частично. Углубляться в тему ссылок и перемещения можно вечно и дискуссия не потухнет еще долго, оставлю лишь ссылку на отличный материал по этой теме http://scrutator.me/post/2018/07/30/value_vs_reference.aspx (тут каждый делает выводы сам, очевидного ответа там нету).
На счет интерфейса класса я с вами согласен, что было б правильней передавать аргумент сообщения напрямую в Print
, но я хотел свести все к "финальной версии", где у нас должно существовать именно поле которое является обьектом Objective-C и функции которые должны взаимодействовать с этим полем, по этому пример и не совсем правильный по дизайну.
На счет, управлением памяти, вы действительно правы и ваше предложение с retain действительно имеет место быть. Но указателем на PrinterImpl
, теоретически владеть кто-то еще может, но фактически он под unique_ptr
, что означает если ты достаешь от туда сырой указатель unique_ptr::get()
то ты получишь "не владеющий" указатель, unique_ptr
для того и создан чтобы не было двух владеющих указателей на один участок памяти.
Среду с автоматическим подсчетом ссылок я действительно не брал в расчет, так как подразумевал что С++ классы будут вызываться и использоваться только в среде С++, когда я писал статью я не рассматривал что другой Objective-C++ код будет их использовать.
но фактически он под unique_ptr
но что мешало кому-то другому им владеть до того, как из него сделали unique_ptr
?
Как бы там ни было, освобождение ресурсов всё равно было некорректным. И как правило, когда кто-то делает release
, ему же стоило заранее сделать retain
.
Наконец, я бы порекомендовал побольше изучить нюансы управления памятью в Objective-C++, ну или же просто поэкспериментировать. Я так понимаю, что всё-таки основное назначение unique_ptr
это скорее нежелание делать деструктор и самостоятельно освобождать ресурсы. Есть вероятность, что unique_ptr вообще не нужен в некоторых сетапах.
Objective-C++ имеет больше фич, чем просто "собрать файл со смесью двух языков". Ещё вроде было можно смешивать данные в коллекциях (ObjC объекты в c++ коллекциях и наоборот), использовать лямбды вместо блоков, и что-то подобное. Я сам мало пользовался ObjC++, но я предполагаю, что управление памятью это одна из основных проблем, которая должна была бы на каком-то уровне да решаться.
К сожалению, документацию по Objc++ найти практически невозможно. https://clang.llvm.org/docs/AutomaticReferenceCounting.html – тут конечно что-то упоминается, но без особых деталей.
Вот какая-то археологическая раскопка, в которой хотя бы есть какие-то примеры: http://web.archive.org/web/20101203170217/http://developer.apple.com/library/mac/#documentation/Cocoa/Conceptual/ObjectiveC/Articles/ocCPlusPlus.html.
Я это всё нашёл в этом треде: https://developer.apple.com/forums/thread/76892.
Углубляться в тему ссылок и перемещения можно вечно и дискуссия не потухнет еще долго, оставлю лишь ссылку на отличный материал по этой теме
Но что будет, если я передам гигабайтную rvalue строку в конструктор? Вместо мгновенного "поглощения" мне может не хватить ещё одного гигабайта памяти. Зачем самому себе мешать жить?
А что если я на вход передам lvalue и эта строка будет потом использоваться в других объектах и классах (везде ее перемещать?), напоминаю, перемещение Не бесплатно.
Написал маленькую программку, и да, я согласен. Почему-то меня с утра замкнуло, и я вдруг решил, что в T&&
всё-таки можно передавать и lvalue тоже, якобы "оно само скопируется если надо, и превратится в rvalue", что очевидно глупость.
Но не лучше ли тогда всё же явно сделать два конструктора? Разве есть какая-то гарантия того, что компилятор будет хотя бы пытаться не копировать из const&
? Неприятно конечно два раза писать одно и то же, но если это всё же имеет практический смысл, то я бы не ленился.
Но ссылка на статью вроде хорошая, правда там надо прям очень медленно читать, добавлю себе в закладки, спасибо.
Прочитал вторую половину статьи несколько раз, и, честно говоря, в очередной раз пришёл к выводу, что я уже вообще перестал понимать что происходит в С++ :) Так что теперь я уже жалею о том, что начал что-то там затирать про сигнатуры, хотя мне всё ещё кажется разумным в явном виде декларировать и себе, и другим, и компилятору о своих реальных намерениях и ожиданиях от выполнения. Мне почему-то не очень нравится идея того, что какой-то определённый компилятор начиная с какого-то года, иногда, может оптимизировать и превратить что-то, что должно было бы быть медленным, во что-то быстрое.
Мне вспомнилось это видео: https://www.youtube.com/watch?v=FJJTYQYB1JQ
Если я всё правильно помню, то там Андрей Александреску говорит о том, что в современном мире верить можно только замерам, а все предположения о "быстром" и "медленном" часто оказываются ложными на практике.
Хех, а с шаблонной магией и forwarding ссылками вообще красота). Мне кажется что любая статья где хоть как-то участвует с++, сводится к какому-то недопониманию и непринятию :), в том числе это и тема оптимизации. Эх **неуверено говорит** раньше было лучше (и добавляет флаг std=c++20) :)
Не очень ясно, включен ли Automatic Reference Counting.
Если нет, то [[NSString alloc] initWithCString: message.c_str() encoding:NSUTF8StringEncoding]
не хватает autorelease
, а классу PrinterImpl
– dealloc
и освобождения ресурсов.
Если же ARC включен, то PrinterImpl.message
должен по-хорошему не доживать до использования, потому что у @property
модификатор assign
вместо retain
или strong
. assign
не увеличивает счётчика ссылок, а значит только что созданный NSString
должен был бы автоматически умереть, так как никто не держит сильной ссылки на него.
Вне зависимости от включенности ARC, свойство должно было быть объявлено как @property (retain/strong) NSString *message;
ARC - нет
В не-ARC среде (и в бородатые времена) класс бы выглядел так:
@interface PrinterImpl: NSObject
// объявлять ivar уже не нужно
@property (retain) NSString *message;
- (instancetype)initWithMessage:(NSString *)message;
- (void)print;
@end
@implementation PrinterImpl
// synthesize уже не нужен,
// и вроде как он сгенерирует ivar "_message", а не "message"
- (void)dealloc {
[_message release];
[super dealloc];
}
- (instancetype)initWithMessage:(NSString *)message {
self = [super init];
if (self) {
// запись в ivar, чтобы избежать возможных сайд-эффектов
// когда перегружен setMessage
_message = [message retain];
}
return self;
}
Как я и думал, если включить ARC, то жизнь немного упрощается. Вот мой пример:
PrinterImpl
– ObjC принтер.
// PrinterImpl.h
#import <Foundation/Foundation.h>
@interface PrinterImpl: NSObject
@property (nonatomic, strong) NSString *message;
- (instancetype)initWithMessage:(NSString *)message;
+ (instancetype)printerWithMessage:(NSString *)message;
- (void)print;
@end
#import "PrinterImpl.h"
@implementation PrinterImpl
- (void)dealloc {
NSLog(@"PrinterImpl dealloc");
}
- (instancetype)initWithMessage:(NSString *)message {
self = [super init];
if (self) {
_message = message;
}
return self;
}
+ (instancetype)printerWithMessage:(NSString *)message {
return [[self alloc] initWithMessage:message];
}
- (void)print {
NSLog(@"message: %@", self.message);
}
@end
Его можно отдельно расширить поддержкой std::string
:
// PrinterImpl+Cpp.h
#import "PrinterImpl.h"
#import <string>
@interface PrinterImpl(Cpp)
- (instancetype)initWithMessageStdString:(const std::string&)message;
+ (instancetype)printerWithMessageStdString:(const std::string&)message;
@end
// PrinterImpl+Cpp.mm
#import "PrinterImpl+Cpp.h"
@implementation PrinterImpl(Cpp)
- (instancetype)initWithMessageStdString:(const std::string&)message {
return [self initWithMessage:[[NSString alloc] initWithCString:message.c_str()
encoding:NSUTF8StringEncoding]];
}
+ (instancetype)printerWithMessageStdString:(const std::string &)message {
return [[self alloc] initWithMessageStdString:message];
}
@end
Принтер на C++, который использует "двойной impl". Printer::Impl
только лишь имеет __strong PrinterImpl *impl_
сильную ссылку на Objective-C объект, жизненный цикл которого будет управляться ARC. т.е. PrinterImpl
будет автоматически послан release
деструктором Printer::Impl::~Impl
.
// Printer.hpp
#pragma once
#include <memory>
#include <string>
class Printer {
public:
Printer(const std::string& message);
~Printer();
void Print() const;
private:
class Impl;
std::string message_;
std::unique_ptr<Impl> impl_;
};
// Printer.mm
#import "Printer.hpp"
#import "PrinterImpl+Cpp.h"
class Printer::Impl {
public:
Impl(PrinterImpl* impl): impl_(impl) {}
__strong PrinterImpl *impl_;
};
Printer::~Printer() = default;
Printer::Printer(const std::string& message):
message_(message),
impl_(std::make_unique<Impl>([PrinterImpl printerWithMessageStdString:message]))
{}
void Printer::Print() const {
[impl_->impl_ print];
}
Убедимся, что соседние файлы могут использовать Printer
будучи обычным C++ кодом:
// User.hpp
#pragma once
#include <string>
void MyPrint(const std::string& message);
// User.cpp
#include "User.hpp"
#include "Printer.hpp"
void MyPrint(const std::string& message) {
Printer(message).Print();
}
И, наконец, main.mm
, который неспроста .mm
, так как кто-то должен создать Autorelease Pool для Objective-C объектов, чтобы управление их памятью работало корректно. Без создания Autorelease Pool PrinterImpl
не удалится. Чтобы убедиться в этом, я добавил -[PrinterImpl dealloc]
, который печатает сообщение при вызове.
#include "User.hpp"
int main(int argc, const char * argv[]) {
@autoreleasepool {
MyPrint("hello world");
}
return 0;
}
К сожалению, я не смог быстро найти как делать кат в комментариях, поэтому извиняюсь за разорванный монитор.
К вашему примеру и не придерешься, но все таки тут немного другая суть, из-за ARC получается что мы используем С++ в Objective-C, а у меня стояла задача именно Objective-C в С++. Что я конкретно имею ввиду: Если у меня сборка под macOS, я нахожу компилятор objective-C и линкую Objective-C обьектник, если же под Линукс (к примеру) то я даже не думаю про Objective-C, а просто забиваю на компиляцию данного обьектника и использую какой-то другой. В вашем же примере мне и под MacOS и под Linux, стоит иметь Objective-C, да и проект становится написанным на Objective-C а не на С++ по факту.
Но ваш пример определенно красивый.
Я не очень понял. Ведь вне зависимости от того, хотим ли мы использовать С++ в Objective-C, или наоборот – оба сетапа называются "Objective-C++".
В линуксе точно так же можно забить на Objective-C, просто опционально выкинув @autoreleasepool
из main.m
. Но весь остальной код ведь тот же самый, разве нет? Принтер на C++ такой же, его C++ версия impl практически та же самая, Objective-C часть тоже. Только управление памятью автоматическое.
Я имею ввиду что в системе которой нету Objective-C ваша программа не скомпилируется: ```gcc: fatal error: cannot execute ‘cc1obj```, а моя (если я ей скажу) чтобы вместо .mm для macOS, я использовал какой-то другой .cpp, для одного и того же хедера - скомпилируется.
Только если использовать 2 разных main для Linux (main.cpp и другой принтер .cpp) для macOS (main.mm и принтер .mm)
Наверное, если хочется кроссплатформенности, то можно перенести Print()
в C++-Impl, и тогда можно использовать разные реализации на разных платформах. А сам Printer
останется неизменным. Но может я что-то не так понимаю в конечной цели.
#include "Printer.hpp"
#if (__APPLE__)
#import "PrinterImpl+Cpp.h"
class Printer::Impl {
public:
Impl(const std::string& message): impl_([PrinterImpl printerWithMessageStdString:message]) {}
__strong PrinterImpl *impl_;
void Print() {
[impl_ print];
}
};
#else
#include <iostream>
class Printer::Impl {
public:
Impl(const std::string& message): message_(message) {}
std::string message_;
void Print() {
std::cout << message_ << "\n";
}
};
#endif
Printer::~Printer() = default;
Printer::Printer(const std::string& message):
message_(message),
impl_(std::make_unique<Impl>(message))
{}
void Printer::Print() const {
impl_->Print();
}
Ещё обратил внимание на эту строку:
К чему этот надуманный пример с принтером? Вместо NSString в последнем примере может быть любой объект из Apple SDK или с любой другой Objective-C библиотеки
Если это "любая из системных" библиотек, то ладно. Но если захочется использовать какую-то стороннюю библиотеку, то как минимум её придётся с 99% вероятностью всё-таки собирать с ARC. Это не обязывает включать ARC и в Objective-C++ прослойке, но тем не менее.
Ещё я опасался, что Objective-C код, который использует weak
может не собраться в не-ARC, однако же я узнал о -fobjc-weak
(CLANG_ENABLE_OBJC_WEAK
).
Compiles Objective-C code to enable weak references for code compiled with manual retain release (MRR) semantics.
Тут у меня явного ответа нету, скажу лишь что когда я хочу писать на С++ и использовать инструменты которые написаны на Objective-C последнее что я хочу видеть, это миграцию всего проекта на Objective-C++ и замена .cpp на .mm.
Ничего не понятно.
Вот есть проект, 1000 cpp файлов. на -x c++. добавляем 1001 файл, .mm или .cpp, не важно, -x objective-c++, либо весь проект собираем как objective-c++ и используем его фичи только в .mm не важно.
В общем, получаем 1001-й файл в котором у нас Objective-C++. ну и используем для него плюсовый хедер и эппловое Objective-C апи. все отлично. платоформо специфичные штуки живут в отдельном файле.
У нас в проекте используется чет около 40 фрейморков из Apple SDK, пока вообще не видел проблем с такой схемой.
При этом сам код в mm файлах имеет базовую структуру С++ кода, с плюшками objective-c только там где без вызова сигналов прям уже ваще никак.
Так получаем код который могут сопровождать разработчики которые знают C++, а objc не очень. А ваше решение, ну не знаю. Если вы 1 его и поддерживаете, то вроде как и норм
Опять таки, ваше решение не подходит для поставленной мною задачи: Мне нужно чтоб код который написаный на С++ мог использовать Objective-C в macOS среде для определенного хєдера, и c++ в Linux среде (Там где нету доступа к Objective-C и AppleSDK) для того же хєдера. Если я перепишу весь проект на .mm файлы он не соберется на линуксе.
Так получаем код который могут сопровождать разработчики которые знают C++, а objc не очень
Мне наоборот кажется что мое решение очень хорошо разграничивает C++ и Objective-C, так как:
1) Objective-C имплементация полностью инкапсулирована PIMPL-ом
2) Это код который может собирается отдельным обьектником (библиотекой если там много файлов)
код который написаный на С++ мог использовать Objective-C в macOS среде для определенного хєдера,
Я что-то не понял, и как он не может использовать?
ну так и мое предложение соответствует 1 и 2.
разница только в том что инкапсулированный код так же пишется на С++ с objc вставками ,а не целиком файл на objc, вот и все. и Pimpl я ровно так же предлагаю
Давайте разберемся, может я вас не правильно понял. У меня есть хэдер файл который должен печатать строчку следующим образом:
1) Для Linux должна быть реализация которая вызывает std::cout
2)Для MacOS соответственно NSLog
3)Реализация должна хранить в себе поле (std::string, NSString соответсвенно)
Этот хедер вызывается из с++ кода (который должен работать и на Linux и на macOS)
Ограничения:
Linux абсолютно не может компилировать Objective-C/Objective-C++ код
Никаких #ifdef APPLE, все решается на этапе сборки (Как в файле ниже, не важно Make, CMake)
Makefile:
UNAME := $(shell uname)
ifeq ($(UNAME), Darwin)
printer_source := printer.mm
else
printer_source := printer.cpp
endif
printer.o: $(printer_source)
...
bin: ... printer.o
...
Как ваш код решит такую проблему ? (как по мне достаточно классическая проблема для кросплатформенного кода который использует Apple SDK)
разница только в том что инкапсулированный код так же пишется на С++ с objc вставками ,а не целиком файл на objc, вот и все
касаемо компиляции какой-то единицы в виде objc++ только под эппл я вообще в 1 комменте и написал. не про разделение файлов речь, а про использование С++ синтаксиса по максимуму. Зачем методы оформлять в этом одном mm файле у вас, вот что я в толк не возьму
Ладно, не буду уже по три раз повторять одну и ту же мысль, не донёс да не донёс.
Я кажется вас понял :), это я для примера все в один mm засунул, в реальности есть еще хедер в котором интерфейс уже (Objective-C) класса, и .m файл с implementation
Допишите про это в статью :) зря наверное упростили. Из благих побуждений с водой выплеснули дитя, видите, я тупой, чего-то важного от вас не уловил.
Теперь вопрос, а почему у вас реализация-то, m ,, а не mm? вы делаете эпл онли код. пимпл, все дела. хедер - C++ файл с методами.
Зачем сами методы писать НЕ С++ кодом, вот в чем мой вопрос? я как разработчик кроссплатформенной кодовой базы, хочу как можно меньше видеть специфичного кода. платформенного кода. objective-c кода. поэтому в mm файле я пишу обычные плюсовые методы и фигачу вызовы objective-c объектов (да, сами объекты из NextStep и прочих API можно разместить в pimpl).
Это хороший вопрос, на него у меня ответа нету :)
Наверное у меня работает наоборот, я это все намудрил потому что хочу видеть Objective-C API исключительно в свойственной для него среде (Objective-C). И использовать Objective-C++ чисто как адаптер между Objective-C и C++.
Да и скорее всего когда человек отркывает .m файл он наверное ожидает видеть там Objective-C код а не Си с вызовами Objective-C API (Тоже самое и с Objective-C++ и С++).
Реальной кодовой базы с примерами подобных патеров у меня нету, по этому это исключительно мое собственное мнение и видиние
// P.h
#if defined __APPLE__
@class Impl;
#else
class Impl;
#endif
class P {
Impl* pimpl;
void f();
};
// P.mm
#include "ImplObjC.h"
#import <Foundation/Foundation.h>
void P::f() {
NSString* str = pimpl->UseObjC();
}
// main.cpp
#include "P.h"
int main() {
P p;
p.f();
return 0;
}
Можно просто использовать forward declaration
Интересно было почитать в каментах как уважаемые мной присутствующие здесь программисты с "боями пробились" к тому очевидному факту, который я изложил сразу в первом же комментарии к статье: "проблемы с использованием Objective-C++ совместно с C++" не существует, равно как и не существует проблемы использования Objective-C++ с C и не существует проблемы использования Objective-C с С.
Используем Objective-C в C++ без проблем