C++ тілінде стандартты кітапхананы (немесе STL) немесе Qt пайдалансаңыз да, жолды біріктіруді орындау үшін оператор+ болуы әдеттегідей. Бұл келесі үзінді сияқты нәрселерді жазуға мүмкіндік береді:
QString statement{"I'm not"}; QString number{"a number"}; QString space{" "}; QString period{". "}; QString result = statement + space + number + period;
Бірақ мұның үлкен кемшілігі бар - уақытша аралық нәтижелерді қажетсіз жасау. Атап айтқанда, алдыңғы мысалда + операторының нәтижесін және өрнектің бос бөлігін сақтауға арналған бір уақытша жол бар, содан кейін бұл жол басқа уақытша жолды қайтаратын санмен біріктіріледі. Содан кейін екінші уақытша сызық нүктеге қосылады, ол түпкілікті нәтиже береді, содан кейін уақытшалар жойылады.
Бұл оператор+ қызметіне қоңырау шалу сияқты қажетсіз бөлулер мен бөлімшелердің көп екенін білдіреді. Сонымен қатар, бірдей деректер бірнеше рет көшіріледі. Мысалы, оператор жолының мазмұны алдымен бірінші уақытша нысанға көшіріледі, содан кейін бірінші уақытша нысаннан екіншісіне, содан кейін екінші уақытша нысаннан соңғы нәтижеге көшіріледі.
Уақытша айнымалылар
Мұны түпкілікті нәтижені сақтау үшін қажет жадты алдын ала бөлетін жол данасын жасау арқылы әлдеқайда тиімді түрде жасауға болады, содан кейін біз біреуін біріктіргіміз келетін жолдардың әрқайсысын қосу үшін QString::appendmember функциясын ретімен шақырады. бірі бойынша:
QString result; result.reserve(statement.length() + number.length() + space.length() + period.length(); result.append(statement); result.append(number); result.append(space); result.append(period);
Уақытша айнымалылар
Немесе, QString:: резервінің орнына QString::resize қолдануға болады, содан кейін оған деректерді көшіру үшін std::copy(немесе std::memcpy) пайдаланыңыз (кейінірек std::copy жолын біріктіруге қалай қолданылатынын көреміз) . Бұл өнімділікті біршама жақсартуы мүмкін (компиляторды оңтайландыруға байланысты), себебі QString::append жолдың сыйымдылығы нәтижесінде алынған жолды ұстау үшін жеткілікті үлкен екенін тексеруі керек. std::copyalgorithm-де оған шамалы артықшылық бере алатын қажетсіз қосымша тексеру жоқ.
Бұл тәсілдердің екеуі оператор+ пайдаланудан айтарлықтай тиімдірек, бірақ бірнеше жолды біріктіргіңіз келген сайын мұндай кодты жазу тітіркендіргіш болар еді.
Алгоритм std::жинақтау
Qt бұл мәселені қазір қалай шешетінін және оны C++17 алған керемет мүмкіндіктермен Qt 6-да жақсартудың мүмкін әдісін жалғастырмас бұрын, стандартты кітапханадағы ең маңызды және қуатты алгоритмдердің бірін қарастырайық, std алгоритмі: :жинақтау .
Біз оларды бөлек айнымалыларда емес, біріктіргіміз келетін жолдардың берілген тізбегін (мысалы, aQVector) елестетіңіз.
std::accumulate көмегімен жолды біріктіру келесідей болады:
QVector<QString> strings{ . . . }; std::accumulate(strings.cbegin(), strings.cend(), QString{});
Алгоритм осы мысалда күткен нәрсені жасайды - ол бос QString-тен басталады және оған вектордан әрбір жолды қосады, осылайша біріктірілген жолды жасайды.
Бұл біріктіру үшін operator+ пайдаланудың бастапқы мысалы сияқты тиімсіз болар еді, өйткені std::accumulate әдепкі бойынша оператор+ ішкі пайдаланады.
Бұл іске асыруды оңтайландыру үшін, алдыңғы бөлімдегідей, сіз онымен толық біріктіруді орындаудың орнына, алынған жолдың өлшемін есептеу үшін жай ғана std::accumulate пайдалана аласыз:
QVector<QString> strings{ . . . }; QString result; result.resize( std::accumulate(strings.cbegin(), strings.cend(), 0, [] (int acc, const QString& s) { return s.length(); }));
Бұл жолы std::accumulate бастапқы 0 мәнінен басталады және жолдар векторындағы әрбір жол үшін сол бастапқы мәнге ұзындықты қосады және соңында вектордағы барлық жолдардың ұзындықтарының қосындысын қайтарады.
Бұл көптеген адамдар үшін std::accumulate дегенді білдіреді - кейбір жинақтау . Бірақ бұл өте қарапайым көзқарас.
Бірінші мысалда шынымен вектордағы барлық жолдар жинақталған . Бірақ екінші мысал сәл басқаша. Шындығында, вектордың элементтері қосылмайды. Векторда QStrings бар және бүтін сандар қосылады.
std::accumulate қуаты мынада, оған теңшелетін операцияны беруге болады. Операция бастапқы жинақтан бұрын жинақталған мәнді және бір элементті алады және жаңа жинақталған мәнді жасайды. Бірінші рет std::accumulate бұл әрекетті шақырады, ол оған жинақтаушы және бастапқы жинақтың бірінші элементі ретінде бастапқы мәнді береді. Ол нәтижені қабылдайды және оны бастапқы жинақтың екінші элементімен бірге операцияның келесі шақыруына жібереді. Бұл бүкіл бастапқы жинақ өңделгенше және алгоритм операцияға соңғы шақырудың нәтижесін қайтарғанша қайталанады.
Алдыңғы код үзіндісінен көріп отырғаныңыздай, аккумулятор вектордағы мәндермен бірдей болуы міндетті емес. Жолдар векторы болды, ал аккумулятор бүтін сан болды.
Жоғарыдағы std::copy алгоритмі көшірілетін элементтер тізбегін (енгізу итератор жұбы ретінде) және элементтер көшірілетін тағайындауды (шығыс итераторы ретінде) алады. Ол тағайындалған жинаққа соңғы көшірілген элементтен кейінгі элементті көрсететін итераторды қайтарады.
Бұл дегеніміз, егер біз бір бастапқы жолдың деректерін std::copy арқылы тағайындалған жолға көшірсек, біз екінші жолдың деректерін көшіргіміз келетін нақты орынды көрсететін иераторды аламыз.
Осылайша, жолды (итераторлар жұбы ретінде) және бір шығыс итераторын қабылдайтын және кейіннен жаңа шығыс итераторын беретін функция бар. Бұл тиімді жолды біріктіруді жүзеге асыру үшін std::accumulate операциясы ретінде пайдалануға болатын нәрсеге ұқсас:
QVector<QString> strings{ . . . }; QString result; result.resize( . . . ); std::accumulate(strings.cbegin(), strings.cend(), result.begin(), [] (const auto& dest, const QString& s) { return std::copy(s.cbegin(), s.cend(), dest); });
std::copy бірінші қоңырау бірінші жолды result.begin() ішінде көрсетілген тағайындалған жерге көшіреді. Ол соңғы көшірілген таңбадан кейін нәтиже жолына итераторды қайтарады және дәл сол жерде вектордан екінші жол көшіріледі. Осыдан кейін үшінші жол көшіріледі және т.б.
Соңында біз біріктірілген жолды аламыз.
Рекурсивті өрнек үлгілері
Енді Qt ішіндегі оператор+ арқылы тиімді жолды біріктіруге қайта аламыз.
QString result = statement + space + number + period;
Жолды біріктіру мәселесі C++ алдыңғы өрнекті кезең-кезеңімен бағалайтынынан, оператор+-ға бірнеше рет қоңырау шалуынан және әрбір қоңырау жаңа QString данасын қайтаратынынан туындайтынын көруге болады.
C++ тілінде мұны бағалау әдісін өзгерту мүмкін болмаса да, өрнек үлгілері деп аталатын әдісті нәтиже жолының нақты бағалауын бүкіл өрнек анықталғанша кейінге қалдыру үшін пайдалануға болады. Мұны оператордың+ қайтару түрін QString емес, бірақ біріктіруді іс жүзінде орындамай-ақ біріктірілетін жолдарды сақтайтын кейбір реттелетін түрге өзгерту арқылы жасауға болады.
Шындығында, егер сіз жылдам жолды біріктіруді қоссаңыз, Qt дәл осылай жасайды 4.6. QString қайтарудың орнына operator+ QStringBuilder деп аталатын жасырын сынып үлгісінің данасын қайтарады. QStringBuilderclass үлгісі оператор+-ға жіберілген аргументтерге сілтемелерді қамтитын жалған түрі ғана.
Төмендегілердің күрделі нұсқасы:
template <typename Left, typename Right> class QStringBuilder { const Left& _left; const Right& _right; };
Бірнеше жолды біріктірген кезде, бірнеше QStringBuilders бір-бірінің ішіне кірістірілген күрделі түрге ие боласыз. Мынадай нәрсе:
QStringBuilder<QString, QStringBuilder<QString, QStringBuilder<QString, QString>>>
Бұл түрі «Мен біріктіру қажет төрт жолды ұстаймын» деп айтудың күрделі тәсілі.
QStringBuilder бағдарламасын QString түріне түрлендіру сұралғанда (мысалы, оны QString нәтижесіне тағайындау арқылы), ол алдымен барлық қамтылған жолдардың жалпы өлшемін есептейді, содан кейін сол өлшемдегі QStringinstance бөледі және соңында жолдарды бір-бірден көшіреді біреуі алынған жолға.
Негізінде ол бұрын жасалған әрекетті жасайды, бірақ ол автоматты түрде, «бармақ көтерудің» қажеті жоқ.
Аргументтердің айнымалы саны бар үлгілер
QStringBuilder бағдарламасының ағымдағы іске асырылуындағы мәселе оның кірістіру арқылы жолдардың ерікті саны бар контейнерді жүзеге асыруында. Әрбір QStringBuilder данасы жолдар немесе басқа QStringBuilder даналары болсын, дәл екі элементті қамтуы мүмкін.
Бұл барлық QStringBuilder даналары жапырақ түйіндері ретінде QStrings бар екілік ағаштың сұрыпталғанын білдіреді. Қамтылған жолдармен бірдеңе жасау қажет болғанда, QStringBuilder өзінің сол жақ ішкі ағашын, содан кейін оң ішкі ағашын рекурсивті түрде өңдеуі керек.
Екілік ағаштарды жасаудың орнына вариалды үлгілерді пайдалануға болады (QStringBuilder жасалған кезде қол жетімді емес C++11 бері қол жетімді). Айнымалы аргумент үлгілері үлгі аргументтерінің ерікті саны бар сыныптар мен функцияларды жасауға мүмкіндік береді.
Бұл std::tuplewe көмегімен сіз қалағаныңызша көптеген жолдарды қамтитын QStringBuilder класының үлгісін жасай алатыныңызды білдіреді:
template <typename... Strings> class QStringBuilder { std::tuple<Strings...> _strings; };
Бізде QStringBuilder-ге қосу үшін жаңа жол болған кезде, біз оны екі кортежді біріктіретін std::tuple_cat арқылы кортежге қоса аламыз (оператор+ орнына % операторын қолданамыз, себебі бұл операторға QStringand QStringBuilder қолдау көрсетеді) :
template <typename... Strings> class QStringBuilder { std::tuple<Strings...> _strings; template <typename String> auto operator%(String&& newString) && { return QStringBuilder<Strings..., String>( std::tuple_cat(_strings, std::make_tuple(newString))); } };
Келісімшарт үлгісінің параметрлері
Мұның бәрі жақсы және жақсы, бірақ мәселе - бүктеме өрнегі (Жолдар бөлігі) пакеттерді қалай өңдейді.
C++ 17 үлгі параметрлерін бүктеу деп аталатын параметр пакеттерін өңдеуге арналған жаңа құрылымды енгізді.
Үлгі параметрін бүктеудің жалпы түрі келесідей (оператор+ басқа екілік оператормен ауыстырылуы мүмкін, мысалы *, %...):
(init + ... + pack)
немесе:
(pack + ... + init)
Бірінші опция үлгі параметрлерінің сол жақ бүктеме өрнегі (сол жақ бүктеме өрнегі) деп аталады, операцияны солға-ассоциативті (солға ассоциативті) ретінде өңдейді, ал екінші опция үлгі параметрлерінің *оң жақ бүктемесі (оң жақ бүктеме өрнегі) деп аталады. * себебі ол операцияны дұрыс -ассоциативті (оң жақтағы ассоциативті) ретінде өңдейді.
Үлгі параметрін бүктеу арқылы үлгі параметрі бумасындағы жолдарды біріктіргіңіз келсе, оны келесідей орындауға болады:
template <typename... Strings> auto concatenate(Strings... strings) { return (QString{} + ... + strings); }
Біріншіден, operator+ QString{} бастапқы мәніне және параметр бумасының бірінші элементіне шақырылады. Содан кейін ол алдыңғы қоңыраудың нәтижесіне және параметр бумасының екінші элементіне оператор+ шақырады. Барлық элементтер өңделгенше және т.б.
Таныс естіледі, солай емес пе?
Дәл осындай әрекет std::accumulate арқылы да байқалды. Жалғыз айырмашылығы - std::accumulate алгоритмі орындалу уақытындағы деректер тізбегі бойынша жұмыс істейді (векторлар, массивтер, тізімдер және т.б.). Үлгі параметрінің бүктемелері компиляция уақытының реттіліктерімен жұмыс істейді, яғни аргументтердің айнымалы саны бар үлгі параметрлерінің пакеттері.
std::accumulate үшін пайдаланылған алдыңғы біріктіру енгізуін оңтайландыру үшін бірдей қадамдарды орындауға болады. Алдымен барлық жол ұзындығының қосындысын есептеу керек. Үлгі параметрінің бүктемелерімен бұл өте қарапайым:
template <typename... Strings> auto concatenate(Strings... strings) { const auto totalSize = (0 + ... + strings.length()); . . . }
Үлгі параметрінің бүктемесі параметр бумасын кеңейткенде, ол келесі өрнекті алады:
0 + string1.length() + string2.length() + string3.length()
Сонымен, біз алынған жолдың өлшемін алдық. Енді біз нәтижеге жеткілікті үлкен жолды шығарып, оған бастапқы жолдарды бір-бірлеп қосуға көшеміз.
Бұрын айтылғандай, үлгі параметрін бүктеу C++ екілік операторларымен жұмыс істейді. Параметрлер бумасының әрбір элементінде функцияны орындағымыз келсе, біз C және C++ тіліндегі ең оғаш операторлардың бірін, үтір операторын пайдалана аламыз.
template <typename... Strings> auto concatenate(Strings... strings) { const auto totalSize = (0 + ... + strings.length()); QString result; result.reserve(totalSize); (result.append(strings), ...); return result; }
Бұл параметр бумасындағы жолдардың әрқайсысы үшін қосымшаны тудырады, нәтижесінде біріктірілген жол болады.
Үлгі параметрінің бүктемелері бар пайдаланушы операторлары
std::accumulate көмегімен қолданылған екінші тәсіл сәл күрделірек болды. Жинақтау үшін арнайы операцияны қамтамасыз ету қажет болды. Аккумулятор (аккумулятор) мақсатты жинақтағы келесі жолды қайда көшіру керектігін көрсететін итератор болды.
Үлгі параметрінің бүктемелерімен теңшелетін операцияны алғымыз келсе, біз екілік операторды жасауымыз керек. std::accumulate файлына жіберілген lambda сияқты оператор бір шығыс итераторы мен бір жолды алуы керек, ол жол деректерін сол иераторға көшіру үшін std::copy деп шақыруы керек және ол келесіні көрсететін жаңа иераторды қайтаруы керек. соңғы көшірілген таңбадан кейінгі элемент.
Ол үшін << операторын қайта анықтауға болады:
template <typename Dest, typename String> auto operator<< (Dest dest, const String& string) { return std::copy(string.cbegin(), string.cend(), dest); }
Барлық жолдарды тағайындалған буферге көшіретін осы іске асырылған үлгі параметрінің бүктеуімен бұл өте оңай болады. Бастапқы мән мақсатты буфердің бастапқы итераторы және оған арналған параметр бумасындағы әрбір жолдың <<:
template <typename... Strings> auto concatenate(Strings... strings) { const auto totalSize = (0 + ... + strings.length()); QString result; result.resize(totalSize); (result.begin() << ... << strings); return result; }
бүктейтін үлгі параметрлері мен кортеждері
Енді біз жолдар жинағын қалай тиімді біріктіруге болатындығын білеміз - бұл вектор немесе параметрлердің вариативтік жиынтығы.
Мәселе мынада, QStringBuilder-де бұл да жоқ. Ол қайталанатын жиын да емес, параметр бумасы да емес std::tuple ішінде жолдарды сақтайды.
Үлгі параметрлер жинағымен жұмыс істеу үшін сізге параметр бумалары қажет. Жолдарды қамтитын параметр бумасының орнына 0-ден n-1-ге дейінгі индекстер тізімін қамтитын буманы жасауға болады, оны кейінірек кортеждегі мәндерге қол жеткізу үшін std::getto көмегімен пайдалануға болады.
Бұл бума компиляция уақытындағы бүтін сандар тізімін көрсететін std::index_sequence көмегімен оңай жасалады. std::index_sequence қабылдайтын көмекші функцияны жасауға болады
template <typename... Strings> class QStringBuilder { using Tuple = std::tuple<Strings...>; Tuple _strings; template <std::size_t... Idx> auto concatenateHelper(std::index_sequence<Idx...>) const { const auto totalSize = (std::get<Idx>(_strings).size() + ... + 0); QString result; result.resize(totalSize); (result.begin() << ... << std::get<Idx>(_strings)); return result; } };
Кортеж үшін индекс тізбегін жасайтын қаптама функциясын жасау керек және concatenateHelper функциясын шақырыңыз:
template <typename... Strings> class QStringBuilder { . . . auto concatenate() const { return concatenateHelper( std::index_sequence_for<Strings...>{}); } };
Қорытынды
Бұл мақала тек нақты жолды біріктіру туралы. Мұны нақты QStringBuilder-ге қолдану үшін тағы бірнеше нәрсе қажет, ал енгізу блог мақаласы ретінде оқу үшін аздап ауыр болады.
Оператордың шамадан тыс жүктелуіне абай болу керек. QStringBuilder бағдарламасының ағымдағы іске асырылуы ретінде std::enable_iflike пайдалану керек, сонда ол Qt біріктірілетін түрлерімен жұмыс істейді және осы операторлармен жаһандық кеңістікті шатастырмайды.
Сондай-ақ, жолды біріктіруге берілген уақытша мәндерді қауіпсіз түрде өңдеу пайдалы болар еді, өйткені QStringBuilder уақытша жолдар жағдайында оңай салбырап тұратын сілтемелерге айналуы мүмкін жол сілтемелерін ғана сақтайды.
Добрый день. Большое спасибо за статью.
А это перевод или авторская статья?
И да, как-то не задумывался над тем, как qt небрежно относится к памяти в таких довольно тривиальных случаях...
Добрый день. Это перевод, в конце статьи указан источник...
Я добавлю в ближайшее время лычку "перевод" к статьям.