Возможно, кто-то, прочитав заголовок, спросит: «Зачем что-то делать со своим кодом?! Ведь С++ кроссплатформенный язык!». В целом, это так… но только пока нигде нет завязок на специфичные возможности компилятора и целевой платформы…

В реальной жизни разработчики, решающие конкретную задачу под конкретную платформу, редко задаются вопросом «А точно ли это соответствует Стандарту С++? А вдруг это расширение моего компилятора». Они пишут код, запускают сборку и чинят места, на которые выругался их компилятор.

В итоге получаем приложение, которое, в некоторой степени, «заточено» под конкретный компилятор (и даже под его конкретную версию!) и целевую ОС. Более того, из-за скудности стандартной библиотеки С++ некоторые вещи просто невозможно написать, не воспользовавшись специфичным API системы.

Так было и у нас в Тензоре. Мы писали на MS Visual Studio 2010. Наши продукты были 32-х битными Windows-приложениями. И, само собой, код был пронизан всевозможными завязками на технологии от Microsoft. Однажды мы решили, что пора осваивать новые горизонты: пора научить СБИС работать под Linux и другими ОС, пора попробовать перейти на другое «железо» (POWER).

В данном цикле статей я расскажу, как мы сделали свои продукты настоящими кроссплатформенными приложениями; как заставили их работать на Linux, MacOS и даже под iOS и Android; как запустили свои приложения на множестве аппаратных архитектур (x86-64, POWER, ARM и другие); как научили работать на big-endian машинах.


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

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

Платформа СБИС написана на С++, но это вовсе не ограничивает прикладного программиста в выборе языка, кроме C++ могут быть использованы JavaScript, Python, SQL.

Наша компания активно развивает свои продукты, поэтому нужно было «чинить поезд на полном ходу» :)


Нужно было работать так, чтобы остальные разработчики не пострадали от нашей деятельности и с комфортом продолжили разрабатывать свой функционал под Windows на MSVC. Это требование сильно повлияло на многие технические решения и сильно усложнило работу.

Для того чтобы у читателя сформировалось представление о масштабности работ, я приведу некоторые цифры:

  • Объем кода нашего фреймворка ~2 миллиона строк
  • Объем «прикладного» кода (код, основанный на платформе СБИС, решающий конкретные бизнес-задачи), сложно оценить, но он в разы превышает объем Платформы
  • Свыше тысячи программистов в десяти центрах разработки

Скучное вступление закончено. Теперь приблизимся к делу и рассмотрим, с какими проблемами мы столкнулись.

Использование API операционной системы


Как упоминалось выше, стандартная библиотека С++ очень скудная, она не включает многих всюду необходимых возможностей. Например, в С++11 нет функционала для работы с сетью… То есть, как только мы захотели сделать простейший HTTP-запрос, мы вынуждены… написать некроссплатформенный код!

Ситуация еще больше усугубляется, если вы используете не самую свежую версию компилятора, как это было у нас — в MSVS 2010 отвратительная поддержка C++11, отсутствует огромная часть нововведений в ядре языка и в стандартной библиотеке.

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

  • Пишем свой класс, с несколькими платформоспецифичными реализациями, основанными на вызовах API целевой системы. Во время сборки препроцессорными директивами ifdef выбираем подходящую реализацию.
  • Используем кроссплатформенные библиотеки — есть множество готовых кроссплатформенных библиотек (опять же, использующие внутри себя платформоспецефичные реализации), которые сильно облегчают нашу задачу. Например, для реализации HTTP-клиента мы взяли cURL.

Особенности реализаций компиляторов


В каждой программе есть ошибки. И компилятор тоже не исключение. Поэтому даже на 100% соответствующий Стандарту код может не собраться на каком-то компиляторе.

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

Что получаем в итоге? Код, который написан четко по Стандарту, может не собраться на каком-то компиляторе; код, который компилируется и работает на одном компиляторе, может не собраться или заработать не так на другом…

Можно перечислить множество проблем этого класса. Вот одна из них:

throw std::exception( "что-то пошло не так" ); // соберется только в MSVC++, так как по стандарту нет такого конструктора

Данный код соберется в MSVC++, так как у них определен дополнительный конструктор:

exception( const char* msg ) noexcept;

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

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

Неопределенное поведение


В Стандарте С++ есть интересный термин «undefined behavior» (неопределенное поведение). Вот его определение из Википедии:
Неопределённое поведение (англ. undefined behavior, в ряде источников непредсказуемое поведение[1][2]) — свойство некоторых языков программирования (наиболее заметно в Си), программных библиотек и аппаратного обеспечения в определённых маргинальных ситуациях выдавать результат, зависящий от реализации компилятора (библиотеки, микросхемы) и случайных факторов наподобие состояния памяти или сработавшего прерывания. Другими словами, спецификация не определяет поведение языка (библиотеки, микросхемы) в любых возможных ситуациях, а говорит: «при условии А результат операции Б не определён». Допускать такую ситуацию в программе считается ошибкой; даже если на некотором компиляторе программа успешно выполняется, она не будет кроссплатформенной и может отказать на другой машине, в другой ОС или при других настройках компилятора.



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

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

Классический пример undefined behavior — это выход за границы массива на стеке. Ниже приведен упрощенный фрагмент кода одного из наших приложений с такой проблемой. Этот баг никак не проявлял себя под Windows в течение нескольких лет и «выстрелил» только после портирования под Linux:

std::string SomeFunction()
{
   char hex[9];
   // some code
   hex[9] = 0; // тут выход за границы массива
   return hex;
}

По всей видимости, MSVS выравнивала буфер на стеке, добавляя после него несколько байт, и при перезаписи чужой памяти мы попадали на пустое, никем не используемое место. А в GCC проблема стала проявляться интересным образом — программа падала далеко от этого кода, в другой функции (видимо, GCC заинлайнил эту функцию, и она стала переписывать локальные переменные другой функции).

Есть и более изящные, трудноуловимые ситуации с UB. Например, на очень интересные грабли можно наступить при использовании std::sort:

std::vector< std::string > v = some_func();
std::sort( v.begin(), v.end(),
   []( const std::string& s1, const std::string& s2 )
{
   if( s1.empty() )
      return true;
   return s1 < s2;
} );

Казалось бы, где тут может быть UB? А все дело в «плохом» компараторе.
Компаратор должен вернуть true, если s1 нужно поставить перед s2. Рассмотрим, что выдаст наш компаратор, если ему на вход подать две пустые строки:

s1 = "";
s2 = "";
cmp( s1, s2 ) == true => s1 должна стоять перед s2
cmp( s2, s1 ) == true => s2 должна стоять перед s1

Таким образом, есть ситуации, когда компаратор противоречит сам себе, то есть не задает strict weak ordering (ссылка на en.wikipedia.org/wiki/Weak_ordering#Strict_weak_orderings). Следовательно, мы нарушили требования std::sort к аргументам и получили неопределенное поведение.

И это не придуманный пример. Такую проблему мы поймали при переходе на Linux. Компаратор с подобной ошибкой работал долгие годы под Windows и… начал рушить приложение с SIGSEGV под Linux (i686). Что интересно, баг ведет себя по разному даже на разных дистрибутивах Linux (с разными GCC на борту): где-то приложение падает, где-то зависает, где-то попросту сортирует не так, как ожидалось.

Зачастую ситуации с undefined behavior можно отловить статическими анализаторами (в том числе и встроенными в компилятор). Поэтому в настройках сборки стоит всегда выставлять максимальный уровень предупреждений. А чтобы не потерять полезный warning в толпе предупреждений вида «неиспользуемая переменная», полезно однажды прибраться в коде, после чего включить опцию сборки «трактовать предупреждения как ошибки», чтобы не допустить появления новых незамеченных предупреждений.

Модели данных


Стандарт С++ не дает никаких жестких гарантий о представлении типов данных в памяти компьютера; он задает лишь некоторые соотношения (например, sizeof(char) <= sizeof(short) <= sizeof(int) <= sizeof(long) <= sizeof(long long)) и предоставляет способы определения характеристик типов.

На различных системах может существенно отличаться способ представления типов. Размерности базовых типов задаются моделью данных. Под моделью данных следует понимать соотношения размерностей типов, принятых в рамках среды разработки. В таблице, представленной ниже, перечислены популярные модели данных и приведены соответствующие ей размерности основных типов С++.


В подавляющем большинстве случаев программисту при выборе типа данных нужны гарантии по поводу его размера. Но на практике разработчики зачастую попросту завязываются на размеры базовых типов в системе, на которой они работают. И вновь при переходе на иную программную или аппаратную платформу мы получаем сюрпризы: какой-то код перестает собираться, какой-то — начинает работать по-другому или вовсе перестает работать.

Например, хэш-функция ниже будет выдавать разные результаты на одних и тех же данных при запуске на различных платформах:

unsigned long some_hash( const unsigned char* buf, size_t size )
{
    unsigned long res = 0;
    for( size_t i = 0; i < size; ++i )
        res = res * buf[i] + buf[i] + i;
   return res; 
}

Большинство таких проблем решаются, если использовать типы с гарантированным размером:

std::int8_t, std::int16_t и т. п.
std::uint32_t some_hash( const unsigned char* buf, size_t size )
{
    std::uint32_t res = 0;
    for( size_t i = 0; i < size; ++i )
        res = res * buf[i] + buf[i] + i;
   return res; 
}

Знаковость char


Полагаю, не многие разработчики задумывались, знаковый ли char. А и если возникал такой вопрос, то большинство открывали свою любимую среду разработки, писали небольшую тестовую программку и получали ответ… верный только для их системы.

На самом деле Стандарт С++ не оговаривает знаковость типа char. Из-за этого есть реализации компиляторов, в которых char знаковый, а есть те, где char — беззнаковый. И это еще одна причина, из-за которой ваша программа может отказаться работать после сборки под другую систему.

Например, этот код работает, как ожидалось, в Linux x86-64, но не работает на Linux POWER (при сборке в GCC с параметрами по умолчанию):

bool is_ascii( char s )
{
   return s >= 0;
}

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

bool is_ascii( char s )
{
   return static_cast<signed char>( s ) >= 0;
}

а в нашем примере можно и вовсе переписать код на битовые операции:

bool is_ascii( char s )
{
   return s & 0x80 == 0;
}

Представление строк


Стандарт С++ никак не регламентирует некоторые аспекты, и каждый компилятор решает эти вопросы по своему усмотрению.

Например, нет никаких гарантий, как будут представлены в памяти строковые константы.
Компилятор MSVS кодирует строковые константы в Windows-1251, а GCC — по умолчанию кодирует в UTF-8.

Из-за таких отличий один и тот же код выдаст разные результаты: strlen("Хабр") в программе, собранной на MSVS выдаст 4; в GCC — 8.

Так же проблемы придут и при вводе-выводе данных. Например, наша тестовая программа умеет сохранять и читать данные в каких-то текстовых файлах:

std::string readstr()
{
   std::ifstream f( "file.txt" );
   std::string s;
   std::getline( f, s );
   return s;
}

void writestr( const std::string& s )
{
  std::ofstream f( "file.txt" );
  f.write( s.c_str(), s.size() );
}

Все будет работать хорошо, пока эти файлы пишут и читают приложения, собранные в одном окружении. Но что будет, если этот файл запишет Windows-приложение, а прочитает приложение под Linux?.. Получим «кракозябры» :)


Как быть в таких случаях? Общий принцип возможных решений один — выбрать какой-то унифицированный способ представления строк в памяти программы и при вводе-выводе делать явное кодирование/декодирование строк. Многие разработчики используют кодировку UTF-8 в своих программах. И это очень хорошее решение.

Но, как я упоминал выше, мы «чинили поезд на полном ходу», и мы не могли сломать некоторые инварианты, на которые полагался наш код (он разрабатывался с учетом, что кодировка строк — Windows-1251):

  • фиксированная ширина символов — возможен произвольный доступ к символу по его индексу
  • есть возможность написания строковых констант на русском языке в коде

В кодировке UTF-8 символы могут представляться различным количеством байт, из-за чего первому требованию она не удовлетворяет. Второе требование в случае UTF-8 не выполняется, к примеру, в MSVC 2010, где строковые константы кодируются в Windows-1251. Поэтому нам пришлось отказаться от UTF-8, и мы решили… полностью абстрагироваться от кодировки, в которой представлены строки, и перешли на «широкие строки» (wide strings).

Такое решение почти полностью удовлетворило наши требования:

  • Практически на всех UNIX-системах «широкие строки» представлены кодировкой UTF-32, то есть ширина символов в ней фиксирована и совпадает с размером элемента типа wchar_t
  • На Windows используется UTF-16. С этой кодировкой дело обстоит несколько сложнее, так как некоторые символы могут быть представлены суррогатными парами. Но, к счастью, все, что есть в Windows-1251, на котором работало наше Windows-приложение, представлено двухбайтовыми последовательностями. Поэтому на начальном этапе мы вовсе не стали поддерживать суррогатные пары и сделали допущение, что под Windows все символы влезают в один элемент типа wchar_t.
  • В С++ можно задавать «широкие» строковые константы, например, L"Привет, хабр!". В этом случае компилятор сам заботится о перекодировке этой строки из кодировки файла исходника в кодировку, в которой представлен wchar_t на целевой системе.

Кроме того, при использовании «широких строк» мы получили ряд преимуществ:

  • В стандартных библиотеках С и С++ есть множество функций и классов для работы с «широкими строками» — нет необходимости писать свои аналоги функций strlen, strstr, классов std::string, std::stringstream и т. п.
  • Многие сторонние библиотеки поддерживают «широкие строки» (например, BOOST)
  • Большая часть WinAPI умеет работать с «широкими строками»

На всех нужных нам платформах «широкие символы» представлены Unicode. За счет этого наши приложения теперь не ограничены латиницей и кириллицей, они поддерживают любые языки мира.

На самом деле борьба с кодировками была самой сложной частью работы по портированию наших продуктов. О ней можно еще очень много рассказать — оставим это для следующих статей :)

Особенности файловых систем ОС


Файловая система Windows имеет несколько отличий от большинства ФС UNIX-подобных систем:

  1. Она регистронезависимая
  2. Она позволяет использовать и символ «\» в качестве разделителя пути

К чему это приводит? Вы можете назвать свой заголовочный файл «FiLe.H», а в коде писать «#include <myfolder\file.h>». В Windows такой код скомпилируется, а в Linux вы получите ошибку, что файл с именем «myfolder\file.h» не найден.

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

Чтобы полностью исключить досадные ошибки, мы повесили на свои git-репозитории простой hook, который проверяет соответствие include-директив этим правилам.

Также особенности ФС влияют и на само приложение. Например,

std::string root_path = get_some_path();
std::string path = root_path + '\\' + fname;

Если у вас есть код, который «клеит» пути через обычные операции конкатенации строк и использует «\» в качестве разделителей, то он сломается, так как под некоторыми ОС разделитель будет воспринят как часть имени файла.

Конечно, можно использовать '/', но в Windows это выглядит некрасиво, да и в общем случае нет гарантий, что не найдется ОС, в которой будет использоваться какой-то иной разделитель.

Чтобы решить эту проблему, мы используем библиотеку boost::filesystem. Она позволяет правильно сформировать путь под текущую систему:

boost::filesystem::path root_path = get_some_path();
boost::filesystem::path path = root_path / fname;

Заключение


Разработка кроссплатформенного ПО на С++ — это нетривиальная задача. Написать программу, которая будет работать на различных программных и аппаратных платформах, не прилагая для этого никаких дополнительных усилий, пожалуй, невозможно. Да и разработать на С++ крупную программу, которая без изменений правильно соберется на любом компиляторе под любую ОС и под любое железо, нельзя, несмотря на то, что С++ — кроссплатформенный язык. Но если вы будете придерживаться ряда правил, которые я вкратце изложил в статье, то вы сможете написать код, который запустится на всех нужных вам платформах. Да и перенести эту программу под новую ОС или железо будет уже не так трудно.

Итого, чтобы писать кроссплатформенный код нужно:

  • Хорошо знать Стандарт С++, понимать, что допускается в нем, а что является расширением конкретного компилятора или вовсе приводит к undefined behavior.

  • Отказаться от использования API системы в коде, инкапсулировав платформоспецифичный код в некоторых классах или воспользоваться готовыми кроссплатформенными библиотеками.

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

  • Определиться с форматом представления строк в памяти программы. Тут может быть много вариантов. Например, использовать UTF-8, как это сделано во многих программах, или вовсе перейти на «широкие» строки, абстрагировавшись от формата представления строк вовсе.

  • Учитывать особенности файловых систем на разных ОС (как в коде, в директивах #include, так и в логике самой программы).

Автор: Алексей Коновалов
Поделиться с друзьями
-->

Комментарии (74)


  1. SBKarr
    19.04.2017 13:10
    +3

    Общие фразы это конечно хорошо, но нам бы в код пофтыкать…


    1. litchenkova
      19.04.2017 13:24

      В планах еще 4 статьи на заданную тему. Делать один, но очень громоздкий пост не хотелось — пошли поэтапно. Так что дальше будет больше мяса, в которое можно повтыкать)


    1. khim
      19.04.2017 22:58
      -1

      Да там и с общими фразами не всё хорошо. Посмотрите на первый абзац: «Зачем что-то делать со своим кодом?! Ведь С++ кроссплатформенный язык!» — и дальше обсуждение того, что это правда — но не совсем правда.

      Так вот: это совсем неправда. C++ — не был задуман «кроссплатформенным языком». Огромное количество способов сделать программу непереносимой. Начиная с того, что никому незвестно априори — влезет ли -1 в char или нет.

      Однако он предназначен для написания кроссплатформенного кода. Но… только и исключительно в случае когда программист использует его «правильно». В этом — принципиальное отличие от Java или, скажем, Go.


      1. tensor_sbis
        21.04.2017 13:37
        +1

        Не соглашусь с вами. Все правила в Стандарте С++ формулируются в терминах абстрактной виртуальной машины, нигде не фигурируют завязки на конкретную программную или аппаратную платформу, то есть формально он является кроссплатформенным. Да и исторически С++ изначально проектировался переносимым, об этом пишет Страуструп в своей книге «Дизайн и эволюция С++» (глава 3.2 «Цели С++»):

        … необходимо было дать ответ на несколько ключевых вопросов:
        * кто будет пользоваться языком?
        * на каких системах будут работать пользователи?

        Чтобы ответить на второй вопрос, я просто огляделся по сторонам — на каких системах реально использовался C with Classes? Практически на любых: таких маленьких, что на них и компилятор-то не мог работать, до мейнфреймов и супер-компьютеров. Среди ОС были и такие, о которых я даже не слыхивал. Отсюда следовали выводы, что высокая степень переносимости и возможность выполнять кросс-компиляцию абсолютно необходимы…


  1. domix32
    19.04.2017 15:42
    +3

    strlen(«Хабр») в программе, собранной на MSVS выдаст 4; в GCC — 8.

    насколько я знаю ни один из них не то что не выдаст, но даже не скомпилирует эти ваши ёлочки


    1. tensor_sbis
      19.04.2017 18:24
      +1

      Спасибо, исправили ошибку


    1. LukeSkyworker
      19.04.2017 22:19
      -2

      Нашли, к чему прикопаться. Может еще к отступам и шрифту начать придираться?
      Никогда не мог понять, почему так мелочно некоторые копошатся, даже не пытаясь оценить статью в целом.


      1. domix32
        19.04.2017 22:30
        +1

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


      1. Areso
        20.04.2017 10:28

        Знаете, я многому учился по книгам и разное видел. Видел программы которые и с «string» работают и с 'string' работают. Одно из двух или вместе. Однажды я натолкнулся на то, что пример простейший на дюжину строк не работает. Потратил уйму времени, психовал. В общем, буря эмоций. К сожалению, среда говорила, что есть ошибка — а какая и где не говорила. Оказалось, что именно там тексты заключаются только в `string` такие кавычки, которые на букве ё висят. Кто же знал?
        Так что да — иногда форма кавычек важна.


        1. Areso
          20.04.2017 10:36

          Парсер ***, ''string'' должно быть в первом случае.


    1. izzholtik
      20.04.2017 10:50

      Это нормально, у AMD в Getting started тоже код никогда компилятора не видел c:
      http://aparapi.com/introduction/getting-started.html


      1. domix32
        20.04.2017 21:40

        Не силён в Java, ничего криминального не увидел.


        1. izzholtik
          26.04.2017 23:01

          final float result = new float[inA.length];
          

          массив пытаются записать во float

          final float inA[];
          ...
          result[i] = intA[i] + inB[i];
          

          неправильно написано имя переменной


  1. NYM
    19.04.2017 17:03
    +1

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


  1. IvaYan
    19.04.2017 20:06
    +1

    Не могли бы вы пояснить, что за СБИС? А то кроме Сверх Больших Интегральных Схем на ум ничего не приходит.


    1. Grammidin
      19.04.2017 21:07
      -4

      Вот в этом видео рассказывается про Тензор и Сбис
      https://youtu.be/jqxxZIkzi80


      1. redmanmale
        19.04.2017 23:17

        СБИС = Система Бухгалтерской И Складской отчетности.


        P.S. Почему бы не написать сразу, если знаете? Зачем давать сслыку на тупой промо ролик?


    1. tensor_sbis
      19.04.2017 22:18

      Изначально СБИС — Система бухгалтерского и складского учета. Название осталось прежнее, а система выросла. Поэтому СБИС мы не расшифровываем. Что такое СБИС, и какие представляет сервисы, можно посмотреть на сайте системы


      1. domix32
        19.04.2017 22:33

        Классно было бы куда-нибудь забить эту расшифровку. Например, вместо подписи "пользователь" в профиле. Слишком сильна ассоциация с интегральными схемами.


  1. vitaly_KF
    19.04.2017 23:22
    +1

    Уверен, что работа проведена масштабная, хочется почитать остальные статьи, но… Зачем вы переизобрели Qt? В крайнем случае могли бы его форкнуть и развивать, но вот так… Честно сказать я офигел))


    PS: для кроссплатформенной работы с сетью есть обычные сокеты. Curl немного облегчает задачу, но вы ведь все делаете по хардкору)


    1. vitaly_KF
      19.04.2017 23:29

      Ах да, еще pimpl вместо ifdef хорошо бы использовать


      1. tensor_sbis
        21.04.2017 13:43
        +1

        Выбрать подходящую имплементацию во время сборки можно по сути только двумя способами:

        1. через ifdef, например:

        #ifdef LINUX
        #  include <my_class_linux_impl.h>
        #elif defined WIN32
        #  include <my_class_win32_impl.h>
        ...
        

        2. В сборочном скрипте, например, в cmake как-то так:

        if (LINUX_OS)
           SET(IMPL_FILES my_class_lunux_impl.cpp)
        elseif (WIN32_OS)
           SET(IMPL_FILES my_class_win32_impl.cpp)
        ...
        endif ()
        add_library(TestLibrary ${IMPL_FILES} another_file1.cpp another_file2.cpp)
        

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

        void some_function()
        {
        #ifdef WIN32
        CallWinAPIMethod();
        #elif defined POSIX
        CallPOSIXMethod()
        #endif
        CallAnotherMethod();
        #ifdef WIN32
        CallAnotherWinAPIMethod();
        #elif defined POSIX
        CallAnotherPOSIXMethod()
        #endif
        }
        

        Мы знаем и активно используем паттерны проектирования (в том числе и pimpl). ifdef'ы используем только для выбора подходящей имплементации.


    1. redmanmale
      20.04.2017 00:55
      +1

      Вы забываете про фатальный недостаток (NIH).


    1. VioletGiraffe
      20.04.2017 09:58
      -1

      Qt — тяжёлая и объёмная штука. Например, мы используем Qt исключительно для «тонкого» UI на Win / Mac / Linux, а для работы с сетью и с файловой системой у нас свои несложные классы-обёртки, которые абстрагируют особенности платформы и работают, кроме вышеуказанных систем, на Android и iOS. Не буду утверждать, что это наилучший подход, или даже рекомендовать его другим, но он меня вполне устраивает. В итоге, у нас минимум зависимостей от сторонних библиотек и максимум легко портируемого кода, который требует наличия только stdlib и системных API.


      1. bamovetz
        20.04.2017 11:39
        +2

        Вот уже не первый раз вижу полное не понимание Qt.
        Уже с 4 версии он совсем не GUI-библиотека. Это полноценный кроссплатформенный фреймворк.
        По поводу объемности — снова незнание структуры Qt. Как раз ваши классы обертки можно полностью заменить QtCore. Получив из коробки платформонезависимый код (работающий так же на Android и iOS) для файловой системы, unicode-строк, многопоточности и много еще что. Причем оверхед там минимальный. Я бы еще понял что вы C++14 используете там наконец много такое появилось (потоки и файлы).
        В виде бонуса с Qt — вы получаете сигналы/слоты и классы для организации собственных структур данных (я про QSharedData и т.п.).


        1. VioletGiraffe
          20.04.2017 12:30

          Я достаточно хорошо понимаю Qt, включая всё, что вы перечислили. Решение отказаться от Qt везде, кроме UI, принято с учётом этого знания.


          1. bamovetz
            20.04.2017 12:49

            Так раскройте причины такого решения. Ибо единственные вами указанны — размер(решается модульностью) и минимум зависимостей(а как же зависимость от того же Qt для GUI).
            Реальная минимизация зависимостей возможна при отказе от кроссплатформенности (писать на MFC/WinAPI например). Иначе сразу возникает дублирование уже имеющегося — что по настоящему очень мало когда действительно требуется.


            1. VioletGiraffe
              20.04.2017 13:33

              Размер в мегабайтах меня совершенно не беспокоит, по крайней мере применительно к Qt (нужные мне модули весят 10-20 МБ). Пожалуй, не стоило использовать слово «размер». Я имел в виду сложность самого фреймворка и его инфраструктуры, а не объём итоговых бинарников.
              Основная причина: завязываясь на Qt, вы теряете кросс-платформенность за рамками списка поддерживаемых систем. И очень сложно потом переделать на что-то другое. Мне однажды довелось выпиливать Qt из бизнес-логики проекта, написанного на Qt, больше не хочу. Это понадобилось для запуска на платформе, которую Qt не поддерживал.
              Вторая причина — доступность Qt на мобильных платформах, или, по крайней мере, на Андроид (насчёт iOS не знаю деталей) достаточно условна. Да, примеры там собираются и работают. А что, если у меня своё приложение со своими Java-UI и нативной библиотекой? Я слабо представляю, как туда вкрутить QtCore. Не говорю, что это невозможно, но не вижу смысла связываться.

              За 6 лет работы с системой, написанной по принципу тонкий UI + кросс-платформенное ядро + небольшие велосипеды для недостающей системно-зависимой функциональности, я пришёл к выводу о целесообразности и удобстве такого подхода.


              1. bamovetz
                20.04.2017 14:05
                +1

                Про сложность понятно. Хоть и ИМХО вы не правы про инфраструктуру и сложность. Она довольно проста и логична. Я вообще считаю что Qt лучший на данный момент пример использования «классического» ООП.
                А вот по поводу кроссплатформенности за пределами поддерживаемых систем можно подробнее? Ибо список систем довольно большой и мне интересно где вы не могли Qt использовать.
                По поводу Андроида — использовать QtCore в нативной c++ библиотеке вроде не составляет проблемы. Особенно если вы отказываетесь от сигналов/слотов (тогда не надо организовывать Qt-очередь сообщений).
                По поводу велосипедов — я принципиально не согласен. Они применимы только в одном случае — требования железа(производительность, ограниченность ресурсов и т.д.). Во всех остальных случаях они нежелательны в том числе по причине проблем с поддержкой. Смениться основной разработчик и будут проблемы.


                1. vitaly_KF
                  20.04.2017 17:53

                  Поддержу Вас.

                  Сложности могут возникнуть только при необходимости запустить кутешный eventloop, да и то решаемые, в остальном же QtCore такая же подключаемая библиотека как и любая другая, за одним исключением — это хорошо документированное, многофункциональное решение, а не сборная солянка, как тот же boost.

                  Насчет поддержки платформ — так в Qt есть слой архитектурной абстракции, нужно реализовать не так много компонентов, чтоб библиотека завелась на новой платформе (http://doc.qt.io/qt-5/qpa.html, http://doc.qt.io/qt-5/portingguide.html).


              1. oYASo
                24.04.2017 02:18

                сложность самого фреймворка

                Чегоооо? Вы же не серьезно, да?
                Qt — это одна из нескольких больших библиотек в мире C++, которую можно начать использовать прям сразу, не вникая в дебри ее построения и мира плюсов вообще. Студенты без серьезного опыта плюсов осваивают либу без особых проблем. О какой сложности идет речь — прям реально любопытно!

                вы теряете кросс-платформенность

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

                Я слабо представляю, как туда вкрутить QtCore.

                Проинсталлировали вместе с приложением и профит, не?

                небольшие велосипеды для недостающей системно-зависимой функциональности

                Вы чуть выше писали про велосипеды с сетью и ФС. Это, мягко говоря, не могут быть «небольшие велосипеды», если только вы не делаете эхо-сервер.


        1. VioletGiraffe
          20.04.2017 12:48

          P. S. Из всего перечисленного, Unicode-строки — единственное, для чего действительно сложно и нецелесообразно писать велосипед. Но есть другие, более доступные, чем Qt, 3rdparty-решения, которые можно включить себе в проект исходниками.
          Для потоков есть std::thread, для файловой системы был велосипед, теперь жду filesystem в с++17. Слоты/сигналы — очень спорная функциональность, я бы не стал их где-либо за пределами UI и интерфейса ядра для связи с UI, даже если бы они были доступны. В ядре предпочитаю интерфейсы и коллбэки как более понятную и более контролируемую в compile-time связь.


          1. bamovetz
            20.04.2017 12:51

            То есть я правильно понимаю что единственная и главная причина это нежелание нести зависимости от библиотек?
            А почему такие жесткие ограничения?


            1. VioletGiraffe
              20.04.2017 13:40

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


      1. oYASo
        24.04.2017 01:59

        libstdc++ от gcc 5.4.0 весит 1.5Mb, Qt5Core от Qt 5.8.0 весит 5.2Mb. Разница, конечно, в 3.5 раза, но назвать ее критической едва ли возможно. Особенно с учетом возможностей, которые Qt предоставляет. Многие embedded решения отказываются от libstdc++ в пользу Qt.

        Использование своих велосипедов для работы с сетью вызывает улыбку. Либо у вас сеть только номинально, либо вы очень серьезные сетевики с крутым опытом, либо делаете что-то не то. Сеть в серьезном приложении очень часто является самым узким местом, и изобретать тут велосипеды вместо использования проверенных либ типа boost.asio — фундаментальная ошибка в архитектуре ПО.

        с файловой системой у нас свои несложные классы-обёртки

        Готов найти не менее трех проблем в ваших несложных обертках.

        В итоге, у нас минимум зависимостей от сторонних библиотек

        А в чем, собственно, профиты, кроме того, что можно написать «у нас минимум зависимостей от сторонних библиотек»? Какие-то проблемы написать «sudo apt install ...»?


        1. VioletGiraffe
          24.04.2017 07:56
          +1

          Либо у вас сеть только номинально

          Ровно в необходимом объёме. Сокеты, HTTP POST/GET без наворотов.

          Готов найти не менее трех проблем в ваших несложных обертках.

          Зачем? Всё, что нам нужно, работает на пяти ОС.

          Какие-то проблемы написать «sudo apt install ...»?

          Какой ещё sudo apt install на iOS?


          1. oYASo
            24.04.2017 16:06

            А если https? Понятно, что можно все самим писать, но зачем?

            Какой ещё sudo apt install на iOS?

            Ну на целевую платформу все статически собирается. Речь про разработчиков.


            1. VioletGiraffe
              24.04.2017 16:43
              -1

              Если https — проблемка, но в обозримом будущем эта проблема в нашем приложении не возникнет.

              Ну на целевую платформу все статически собирается. Речь про разработчиков.

              Интересная мысль, но в моём проекте вряд ли больше половины из уже имеющихся сторонних библиотек доступны через apt-get (а особенно — для кросс-компиляции под Андроид / iOS).


  1. Videoman
    20.04.2017 01:52

    std::string path = root_path + '\' + fname;
    

    Данная строка не скомпилируется, так как слэш экранирует кавычку. Исправьте пример пожалуйста (для новичков).


    1. tensor_sbis
      20.04.2017 14:47

      Спасибо. Поправили.


  1. Jef239
    20.04.2017 07:41

    На самом деле не все так страшно с ОС. Для всего, кроме творений микрософт, есть POSIX. А вот для Windows и MS-DOS приходится делать отдельные ветки. Впрочем, в windows можно использовать какой-нибудь minGW, а на MS-DOS забить болт.


    1. tensor_sbis
      21.04.2017 13:35
      +1

      Да, программа, которая работает хотя бы на одной современной POSIX-совместимой системе относительно просто переносится на другие POSIX-совместимые системы. Но рынок диктует свои требования и отказаться от поддержки Windows зачастую невозможно.
      А в нашем случае ситуация была еще сложнее — у нас были большие Windows-приложения и нам нужно было портировать их под POSIX. Огромный объем кодовой базы не позволял нам переписать все за раз — это был длительный итеративный процесс, параллельно с которым шло активное развитие этих продуктов. Как минимум из-за этого нельзя было «сжигать» мосты и переходить на POSIX, отказавшись от WinAPI (иначе на какой-то период сломалась бы сборка на MSVS и работа наших коллег бы встала).
      MinGW не решает проблему — он использует WinAPI, ровно как MSVS (сейчас, кстати, мы перешли на него, отказавшись от MSVS). Вы, наверное, имели ввиду cygwin, в котором WinAPI завернут внутрь POSIX-совместимого API.


  1. sborisov
    20.04.2017 10:39
    +2

    Разделитель лучше использовать так

    boost::filesystem::path::preferred_separator
    


  1. justhabrauser
    20.04.2017 10:40
    +1

    «Компилятор MSVS кодирует строковые константы в Windows-1251, а GCC — по умолчанию кодирует в UTF-8» — мне одному это утверждение показалось странным?
    IMHO компилятор ничего не кодирует. Кодировка зависит от кодировки ОС, которая в Linux _обычно_ UTF-8, а в Windows (RU) в GUI-редакторах — CP1251.


    1. pitochka99
      20.04.2017 11:48

      Кодировка зависит от кодировки конкретного файла, в котором объявлена константа.


      1. madkite
        20.04.2017 13:51

        А как, интересно, задается кодировка для "конкретного файла"? Есть, конечно, Byte Order Marks, но это только для Unicode.


        1. pitochka99
          20.04.2017 15:39

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

          Например, если в коде есть такая строка, записаная в cp1251:

          const char * str = "Какая-то строка";
          


          А далее идет такой код:
          std::locale::global(std::locale("C"));
          std::cout << str << std::endl;
          


          То выведется кракозябра.


          1. tensor_sbis
            20.04.2017 16:38

            Нет, в жизни это не так. Все компиляторы перекодируют «широкие» строки, а некоторые — даже «узкие». Чтобы убедиться в этом, можете взять MSVC 2010 и провести опыт: скомпилировать в ней один и тот же файл, закодированный в кодировки UTF-8, UTF-8+BOM и Win1251:

             #include <stdio.h>
            int main()
            {
               printf( "%hhu\n", static_cast<unsigned char>( 'я' ) );
               printf( "%hu\n", static_cast<unsigned short>( L'я' ) );
            }
            


            В случае с Win1251 вы получите ожидаемый результат, который никак не противоречит вашей гипотезе:
            255 (это код символа 'я' в Win1251)
            1103 (это код символа 'я' в UTF-16, которой представлен wchar_t на Win)

            В случае UTF-8 вы получите:
            143 (это один из двух байт UTF-8 представления символа 'я' — подходит под вашу гипотезу)
            1057 (а это ошибка! компилятор неправильно закодировал L'я', так как неправильно воспринял исходник и сделал перекодировку win1251->utf16 вместо utf8->utf16)

            В случае с UTF-8 с заголовком BOM мы имеем результат, идентичный первому варианту:
            255
            1103
            И этот случай противоречит вашей гипотезе — компилятор представил «узкую» строку в 1251, несмотря на то что она была в исходнике в UTF-8.

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


  1. Sjam
    20.04.2017 11:45

    В данном цикле статей я расскажу, как мы сделали свои продукты настоящими кроссплатформенными приложениями; как заставили их работать на Linux, MacOS и даже под iOS и Android


    Отлично. Но где можно посмотреть эти самые продукты? На сайте https://sbis.ru/download нет ни малейшего упоминания о «кроссплатформенности».


    1. tensor_sbis
      20.04.2017 11:46
      +1

      Мобильные приложения СБИС сделаны на нашей платформе

      https://play.google.com/store/apps/details?id=ru.tensor.sbis.droid

      https://itunes.apple.com/ru/app/%D1%81%D0%B1%D0%B8%D1%81/id1156518720

      Остальное — облачные решения;
      Облако sbis.ru построено на платформе


      1. Sjam
        20.04.2017 13:20

        И снова увидел по ссылкам Andoid, iOS и web, какое это отношение имеет к Linux и Windows?

        Т.е. по сути и статьи и вашего ответа решений под Linux нет?


        1. Olegas
          20.04.2017 14:58

          Весь backend облачных решений живет на Linux и основан на платформе


          1. Sjam
            29.04.2017 09:02

            Извините, смотрю с той стороны, которая доступна простому пользователю. И простому пользователю не Windows из перечисленного ничего не доступно. Даже облачная часть sbis требует ActiveX для подписи и шифрования и как следствие без IE не имеет никакого значения.


            1. Olegas
              29.04.2017 09:59

              Облачный СБИС работает не только в IE. И подпись и шифрование работают у нас и в Chrome, и в FireFox.


  1. Videoman
    20.04.2017 12:03

    Подход к строкам описанный @ensor_sbis использовался у нас в конторе, по энерции, довольно долго, но потом я перевел все проекты на UTF-8 и очень рад этому. Основные недостатки старого подхода, на мой взгляд, такие:

    1. У автора статьи сказано, что в wchat_t помещается Unicode символ (я так понимаю подразумевается UTF-32), но на самом деле, целиком умещается только code point, а символ, по прежнему, может кодироваться несколькими code point-ами.
    2. В случае сериализации, особенно в рамках ASCII <= 127 получается жуткий перерасход по памяти.
    3. В случае сериализации появляется понятие byte order внутри code point-a. Следовательно имеем неразбериху и дополнительные накладные расходы на обработку строк.
    4. В рамках исключений С++, даже если очень нужно, невозможно передать локализованной сообщение, так как нет перегрузки для wchar_t.

    Преимущества UTF-8:
    1. Компактно, при сериализации и по памяти.
    2. Нет проблем с порядком байт, можно сразу читать из потока или файла.
    3. Парсеры и форматтеры пишутся точно также как и для всех «узких» строк, на практике можно вообще не обращать внимание что строки в UTF8.
    4. Удобно смотреть дампы пакетов и памяти, так как, опять же, нет вариантов с порядком байт.
    5. И последние, это уже вкусовщина, но мне, лично, удобней и приятней видеть в коде просто кавычки вместо L"..." или L'...'


    1. SBKarr
      20.04.2017 16:06

      А у нас компанийский стандарт: UTF-8 для передачи, обработки и хранения, UCS-2 для отображения и пользовательского ввода. Ибо накладные расходы процессорного времени при пользовательском вводе в UTF-8 (особенно, если с автодополнением, и в произвольное место) сильнее сказываются на субъективной производительности, чем дополнительный расход памяти.


      1. Videoman
        20.04.2017 17:53

        Странно. У меня скорость даже выросла, так как работа со стоками UTF-8 (если это конечно не текстовый процессор), обычно, происходит последовательно и в этом случае она ничем не отличается от работы с простым ASCII.


        1. SBKarr
          20.04.2017 18:49

          При вводе нужна постоянная трансформация из code point в позицию в строке и обратно (для показа выделения и позиции ввода). Если индекс в строке соответствует code point — это быстро. Если нет — либо медленно, либо нужен дополнительный кеш позиции, с нетривиальным алгоритмом обновления при автокомплите. На настольных устройствах всё круто. На мобильных можно заметить снижение FPS при работе с большими строками. Ситуация осложняется разными способами получения результатов ввода на разных платформах.


          1. khim
            20.04.2017 21:50
            +1

            При вводе нужна постоянная трансформация из code point в позицию в строке и обратно (для показа выделения и позиции ввода).
            Вы это серьёзно? Как у вас с поддержкой вещей типа а?? Если же у вас установка, что «в природе существует только русский и английский» — то зачем вообще заморачиваться с Юникодом?

            Если нет — либо медленно, либо нужен дополнительный кеш позиции, с нетривиальным алгоритмом обновления при автокомплите.
            Он, собственно, нужен всегда, когда вы хотите работать с Юникодом.


            1. SBKarr
              21.04.2017 09:14
              -1

              У нас кроме русского и английского практически все европейские языки. Поддержка арабских, азиатских и тому подобных, каюсь, не планировалась. А вся европейская диакретика есть в BMP в виде монолитных символов, к которым можно привести строку в ходе нормализации.

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


    1. Foxeed
      20.04.2017 17:42

      Есть такой бородатый сайт про UTF-8, проще на него ссылку кидать, чем каждый раз писать про преимущества UTF-8. :) И по-хорошему надо всегда использовать только ее для всех внутренних представлений, а если очень необходимо снаружи библиотеки иметь другую кодировку, конвертировать в нее по требованию.


  1. Alesh
    21.04.2017 00:36

    После заявления что в C++11 нет функционала для работы с сеть задумался, а стоит ли продолжать читать)


    1. khim
      21.04.2017 01:01

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

      То есть понятно, что как бы «работа с сетью» — это, наверное, не есть «базовая функциональность языка программирования»… с одной стороны. А с другой — работа с перфокартами входила в ISA IBM 360, где, вроде как, им тоже не совсем место, да?

      Так что не стоит уж прям так сразу «сплеча» рубить…


      1. Alesh
        21.04.2017 02:05
        -1

        Я конечно понимаю что грань между тем что называется стандарт языка и стандартная библиотеки языка очень зыбкая, но она всё-таки присутствует. К тому же это не одно спорное заявление в статье. Следом идёт про то что реализация HTTP запроса делает код на С++ не кроссплатформенным. А я например знаю, как реализовать это кросплатформенно. Правда это будет иметь "фатальный недостаток")


        1. khim
          21.04.2017 04:54
          +1

          А я например знаю, как реализовать это кросплатформенно.
          Оставаясь в рамках C++11 и не используя не-кроссплатформенные API? Хотел бы я на это посмотреть!


          1. Alesh
            21.04.2017 10:18

            Нет конечно, используя сторонние библиотеки, я же написал что будет иметь «фатальный недостаток»)
            Ну а что вам мешало пойти тем же путем? Впрочем вы с таким восторгом рассказываете о своей платформе, поэтому наверно не стоит даже начинать про «зачем писать свой велосипед?»)


            1. Foxeed
              21.04.2017 12:19

              Вы не видите разницы между сторонними библиотеками и тем, что предоставляет чистый C++11?
              Нет в одиннадцатых плюсах ни поддержки сети, ни поддержки файловой системы. И тут почти всегда выруливает буст.


              1. Alesh
                21.04.2017 14:24

                Вы точно на мой пост ответили?)
                Я прекрасно вижу разницу между сторонними библиотеками и стандартной библиотекой С++. Или вы что-то другое имели ввиду во фразе «чистый C++11».
                Кстати С++11 это номер обновления версии стандарта на язык С++, а не что-то отдельное и самодостаточное. Кстати есть уже и С++14, и даже С++17 — работы с сетью в них тоже нет, и думаю не будет никогда. В философии С++ есть фишка — не тащить прикладной код в стандартную библиотеку.


  1. k_simakov
    21.04.2017 14:16

    есть возможность написания строковых констант на русском языке в коде

    Второе требование в случае UTF-8 не выполняется, к примеру, в MSVC 2010, где строковые константы кодируются в Windows-1251.

    Мне кажется, Вы здесь вводите в заблуждение читателей. Строковые константы в UTF-8 прекрасно работают в MSVC 2010. Вам просто нужно превсести все ваши *.h и *.cpp файлы в UTF-8 без BOM и забыть о проблеме с кодировками. Ну и настроить среду так, чтобы новые исходники автоматом создавались в UTF-8.

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

    Из-за него вы перешли на UTF-32, а это большой оверхед. При том, что доступ к символам по их индексам на практике — не такая уж и частая задача. Большинство задач, связанных со строками, можно свести к их обходу с помощью итераторов.


    1. avkonovalov
      21.04.2017 15:30
      +1

      Нет, это не так: https://msdn.microsoft.com/en-us/library/mt708821.aspx

      By default, Visual Studio detects a byte-order mark to determine if the source file is in an encoded Unicode format, for example, UTF-16 or UTF-8. If no byte-order mark is found, it assumes the source file is encoded using the current user code page


      MSVS кодирует «узкие» строки в кодировку локали: https://msdn.microsoft.com/en-us/library/mt589696.aspx
      The execution character set has a locale-specific representation.


      Таким образом, в случае, если студия компилирует файл в UTF-8, она воспринимает его как файл в кодировке локали. Так как она кодирует узкие строки тоже в кодировку локали, преобразование как таковое не требуется => все ваши UTF-8 строки попадают в исходном виде. Но все это здорово работает, пока в файле не встретится код, не существующий в кодировке локали (это 0x98 в случае русскоязычной локали, где кодировка Win1251).

      Кроме того при сборке в MSVS исходников в UTF-8 появляются проблемы с широкими строками — MSVS воспринимает файл как 1251 и выполняет перекодировку Win1251 -> UTF-16 вместо UTF-8 -> UTF-16 и получаем «кракозябры»

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

      В статье писалось об этом, но повторюсь — у нас была огромная кодовая база (десятки миллионов строк кода), в этом коде были завязки на фиксированную ширину символов. В принципе переписать этот код — очень тяжелая задача. Но кроме этого, как уже многократно писалось, шло активное развитие продуктов и нужно было портировать таким образом, чтобы не блокировать работу других разработчиков (нельзя было развалить сборку на недельку год, пока переводим на UTF-8).
      Мы пошли путем microsoft'а — все строки завернули в макрос _T( «строка» ), строковым функциям и типам сделали псевдонимы (наподобие _tcscmp). Благодаря этому мы могли жить, поддерживая сразу два варианта сборки: сборка на узких строках (из которой выпускались все production решения) и сборка на широких строках (которая итерационно развивалась в течение года). В случае, если бы мы стали использовать UTF-8, мы не смогли бы обеспечить такую совместимость.


      1. k_simakov
        21.04.2017 17:53

        Нет, это не так: https://msdn.microsoft.com/en-us/library/mt708821.aspx

        Вы даёте ссылку на документацию к VC 2015, а тут вроде бы речь идёт про 2010. Если бы речь шла про 2015, то проблемы вообще бы не было, просто потому, что у компилятора есть соответствующая опция /utf-8, ссылку на которую Вы и даёте.

        MSVS кодирует «узкие» строки в кодировку локали: https://msdn.microsoft.com/en-us/library/mt589696.aspx.


        Там же написано следующее

        The basic execution character set and the basic execution wide-character set consist of all the characters in the basic source character set...


        Ну а про basic source character set сказано так

        When source files are saved by using a locale-specific codepage or a Unicode codepage, Visual C++ allows you to use any of the characters of that code page in your source code

        Т.е. Microsoft явно разрешает использовать все символы юникода в исходниках.

        Вы не подумаете, что я сильно придираюсь, просто хочу понять, есть ли проблема на самом деле и как её воспроизвести. Собрал только что тестовый консольный проект в VC2010, сохранил исходники в utf-8, создал в коде узкую строку на традиционном китайском языке и никаких проблем с ней не обнаружил.
          const char* TestString = "??";
          printf("%d", strlen(TestString) );
        

        Этот код у меня на экран консоли выдаёт цифру 6


        1. avkonovalov
          21.04.2017 18:14
          +1

          Как минимум с широкими строками проблемы точно будут:

          #include <windows.h>
          
          int main()
          {
              MessageBoxW(NULL, L"Тест", L"Тест", MB_OK);
          }
          


          Результат:

          image


        1. avkonovalov
          21.04.2017 18:29
          +1

            const char* TestString = "??";
            printf("%d", strlen(TestString) );
          

          Кстати говоря, в вашем коде есть неопределенное поведение :) Функция strlen возвращает тип size_t, а формат указан для int. По стандарту это UB:
          … If any argument is not the correct type for the corresponding conversion specification, the behavior is undefined.

          Для size_t надо использовать %z


        1. avkonovalov
          21.04.2017 18:36

          Для старых студий вообще даже issue на багтрекере есть:
          https://connect.microsoft.com/VisualStudio/feedback/details/341454/compile-error-with-source-file-containing-utf8-strings-in-cjk-system-locale

          В этом issue люди словили проблемы из-за того что у них в исходнике встретился код, не входящий в кодировку их локали (о чём я писал выше)