воскресенье, 19 июня 2016 г.

[prog.c++14] Еще один способ конкатенации строк в compile-time

Недавно у себя в G+ ленте я дал ссылку на свою первую попытку реализации конкатенации строк в compile-time: https://godbolt.org/g/GWLTN8. Эта версия работала под GCC, но не работала под clang. Причем, как мне представляется, прав в этой ситуации именно clang: ведь constexpr-функция может быть запущенна не только в compile-time, но и в run-time. И clang мог посчитать, что в каких-то контекстах операции, выполняющие обработку аргументов функции, не могут быть использованы.

Дабы избавиться от этой проблемы я нашел другой способ подсчета размерности результирующией строки. В результате код собирается компиляторами clang 3.5-3.8 и gcc 5.1-6.1 с ключиком -std=c++14. Поиграться с кодом можно здесь: https://godbolt.org/g/G5wdyD (так же полный код примера под катом).

Отдельно стоит сказать про выхлоп компилятора. Очевидно, что содержимое результирующей строки формируется в compile-time, иначе бы не работали проверки в static_assert. Но вот как это содержимое добавляется в код зависит от компилятора: clang явно размещает результирующую строку как строковый литерал и затем использует адрес этого литерала. А вот gcc использует серию команд movb для инициализации значения в run-time.

Задачка эта всплыла в большущем LOR-овском флейме. Еще один товарищ из этого обсуждения предложил несколько своих вариантов. Перечислены они здесь: https://www.linux.org.ru/forum/development/12649936?cid=12674932 (пояснительный текст с ссылками на godbolt) и здесь: https://www.linux.org.ru/forum/development/12649936?cid=12675192 (полные тексты). Данные решения используют либо ключи -std=c++1z (т.е. требуют фич из не утвержденного еще стандарта C++17), либо используют GNU-расширения языка. Последнее из решений, в котором используется GNU-тое расширение для формирование compile-time строковых литералов посредством суффикса _ct, дает для clang и GCC одинаковый выхлоп: инициализация строки в run-time через movl.

Какой-то практической значимости у этой форумной задачки не видно. Может быть, подобные фокусы смогут оказаться полезными, когда в программу нужно вшивать base64 представления каких-то строк. Или если в программе нужны строки для представления URL или имен MQ-шных топиков, склеенные из частей, которые известны на этапе компиляции. Но лично я бы в таких случаях предпочел бы pre-compile-time генерацию строковых литералов.

Однако, в плане того, чтобы поразбираться с фичами современного C++ эта задачка очень полезная. Т.к. в интернете можно нагуглить несколько вариантов решения подобной задачи. И далеко не всегда понятно, как и почему это все работает. Ну и самым полезным результатом лично для меня стало открытие вот такой конструкции:

template< std::size_t ...L >
constexpr auto
ct_concat( const char (&...args)[L] )

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

Ну а теперь, собственно, то, что у меня получилось.

Код, наверное, довольно многословный. Но мне так проще писать код и затем проще в нем разбираться.

#include <utility>
#include <iostream>

template< std::size_t N >
struct ct_cstringz
{
   char data_[ N ];

   constexpr char * data() { return data_; }
   constexpr const char * data() const { return data_; }

   constexpr char & operator[]( std::size_t i ) { return data_[i]; }
   constexpr const char & operator[]( std::size_t i ) const { return data_[i]; }
};

namespace ct_details
{

template< std::size_t N >
constexpr char *
copy( const char (&s)[N], char * to )
{
   const char * b = s, * e = s + N - 1;
   while( b != e )
      *(to++) = *(b++);
   return to;
}

template< std::size_t C, std::size_t L, std::size_t... T >
struct size_detector_impl
{
   static constexpr const std::size_t value =
      L - 1 + size_detector_impl< sizeof...(T), T... >::value;
};

template<std::size_t L>
struct size_detector_impl<1, L>
{
   static constexpr const std::size_t value = L;
};

template< std::size_t... T >
struct size_detector
{
   static constexpr const std::size_t value =
      size_detector_impl< sizeof...(T), T... >::value;
};

constexpr char *
copy_to( char * to ) { return to; }

template< std::size_t N, typename... ARGS >
constexpr char *
copy_to( char * to, const char (&s)[N], ARGS &&... args )
{
   char * p = copy( s, to );
   return copy_to( p, std::forward<ARGS>(args)... );
}

/* namespace ct_details */

template< std::size_t ...L >
constexpr auto
ct_concat( const char (&...args)[L] )
{
   using namespace ct_details;

   ct_cstringz< size_detector< L... >::value > result{};

   char * p = copy_to( result.data(), args... );
   *p = 0;

   return result;
}

template< std::size_t N0, std::size_t N1, std::size_t N2 >
constexpr auto
dynamic_domain_name_at_compile_time(
   const char (&s0)[N0],
   const char (&s1)[N1],
   const char (&s2)[N2] )
   {
      return ct_concat( s0, ".", s1, ".", s2 );
   }

int main()
{
   constexpr auto r = ct_concat( "linux""org""ru" );
   static_assert11 == sizeof(r), "Unexpected size!" );
   static_assert'l' == r[0], "Expected 'l'" );
   static_assert'i' == r[1], "Expected 'i'" );
   static_assert'x' == r[4], "Expected 'x'" );
   static_assert'o' == r[5], "Expected 'o'" );

   puts( r.data() );

   const char domain[] = "linux.org.ru";
   constexpr auto dynamic = dynamic_domain_name_at_compile_time(
          "linux""org""ru" );
   static_assertsizeof(domain) == sizeof(dynamic), "Oops!" );

   puts( dynamic.data() );
}

Комментариев нет: