MCRO
C++23 utilities for Unreal Engine.
Loading...
Searching...
No Matches
Conversion.h
Go to the documentation of this file.
1/** @noop License Comment
2 * @file
3 * @copyright
4 * This Source Code is subject to the terms of the Mozilla Public License, v2.0.
5 * If a copy of the MPL was not distributed with this file You can obtain one at
6 * https://mozilla.org/MPL/2.0/
7 *
8 * @author David Mórász
9 * @date 2025
10 */
11
12
13#pragma once
14
15#include <ranges>
16
17#include "CoreMinimal.h"
18#include "Misc/EngineVersionComparison.h"
19
20#include "Mcro/Concepts.h"
23#include "Mcro/Templates.h"
24#include "Mcro/Text.h"
25
27#include "range/v3/all.hpp"
29
30/**
31 * @brief
32 * Restore range-v3 compatibility for `TArray` on UE 5.8+.
33 *
34 * In non-Shipping builds (`TARRAY_RANGED_FOR_CHECKS == 1`) `TArray::begin()`/`end()` return
35 * `TCheckedPointerIterator`. UE 5.8 reworked this iterator so it no longer exposes a `value_type`
36 * typedef, therefore range-v3 0.12.0 cannot compute `iter_value_t`, which makes
37 * `indirectly_readable` (and in turn `input_iterator`) evaluate to `false`. As a result every
38 * view adaptor (`transform` / `filter` / `concat`) and the `array | view` pipe stop resolving.
39 * Supplying the value type through range-v3's own `indirectly_readable_traits` customization
40 * point restores recognition.
41 *
42 * @remarks
43 * Only required on UE 5.8+. Earlier engine versions expose the iterator's comparison operators
44 * directly, so range-v3 deduces the value type on its own.
45 */
46#if !UE_VERSION_OLDER_THAN(5, 8, 0) && defined(TARRAY_RANGED_FOR_CHECKS) && TARRAY_RANGED_FOR_CHECKS
47namespace ranges
48{
49 template <typename ElementType, typename SizeType, bool bReverse>
50 struct indirectly_readable_traits<::TCheckedPointerIterator<ElementType, SizeType, bReverse>>
51 {
52 using value_type = std::remove_cv_t<ElementType>;
53 };
54}
55#endif
56
57namespace Mcro::Range
58{
59 using namespace Mcro::Templates;
60 using namespace Mcro::Concepts;
61
63 {
64 FString Start { TEXT_"[" };
65 FString End { TEXT_"]" };
66 FString Separator { TEXT_", " };
67 };
68
69 namespace Detail
70 {
71 template <CRangeMember Range>
73 {
74 TRangeWithStringFormat(Range const& range, FRangeStringFormatOptions const& options)
75 : Storage(range)
76 , Options(options)
77 {}
78
79 auto begin() const { return Storage.begin(); }
80 auto end() const { return Storage.end(); }
81
83 private:
84 Range const& Storage;
85 };
86 }
87
88 /** @brief Specify a separator sequence for a range when converting it to a string */
89 FORCEINLINE auto Separator(FString const& separator)
90 {
91 return ranges::make_pipeable([separator] <CRangeMember Input> (Input&& range)
92 {
93 if constexpr (CMatchTemplate<Input, Detail::TRangeWithStringFormat>)
94 {
95 range.Options.Separator = separator;
96 return range;
97 }
98 else return Detail::TRangeWithStringFormat(range, {.Separator = separator});
99 });
100 }
101
102 /** @brief Specify a start and an end sequence enclosing this range when converting it to a string */
103 FORCEINLINE auto Enclosure(FString const& start, FString const& end)
104 {
105 return ranges::make_pipeable([=] <CRangeMember Input> (Input&& range)
106 {
107 if constexpr (CMatchTemplate<Input, Detail::TRangeWithStringFormat>)
108 {
109 range.Options.Start = start;
110 range.Options.End = end;
111 return range;
112 }
113 else return Detail::TRangeWithStringFormat(range, {.Start = start, .End = end});
114 });
115 };
116
117 /** @brief Don't use a separator when this range is rendered to a string */
118 FORCEINLINE auto NoSeparator()
119 {
120 return ranges::make_pipeable([] <CRangeMember Input> (Input&& range)
121 {
122 if constexpr (CMatchTemplate<Input, Detail::TRangeWithStringFormat>)
123 {
124 range.Options.Separator = {};
125 return range;
126 }
127 else return Detail::TRangeWithStringFormat(range, {.Separator = {}});
128 });
129 }
130
131 /** @brief Don't enclose this range in anything when it's rendered to a string */
132 FORCEINLINE auto NoEnclosure()
133 {
134 return ranges::make_pipeable([] <CRangeMember Input> (Input&& range)
135 {
136 if constexpr (CMatchTemplate<Input, Detail::TRangeWithStringFormat>)
137 {
138 range.Options.Start = {};
139 range.Options.End = {};
140 return range;
141 }
142 else return Detail::TRangeWithStringFormat(range, {.Start = {}, .End = {}});
143 });
144 }
145
146 /**
147 * @brief
148 * Don't insert anything else than the contents of the input range when that is rendered as a string just append
149 * each item one after the other.
150 */
151 FORCEINLINE auto NoDecorators()
152 {
153 return ranges::make_pipeable([] <CRangeMember Input> (Input&& range)
154 {
155 if constexpr (CMatchTemplate<Input, Detail::TRangeWithStringFormat>)
156 {
157 range.Options = {{}, {}, {}};
158 return range;
159 }
160 else return Detail::TRangeWithStringFormat(range, {{}, {}, {}});
161 });
162 }
163
164 namespace Detail
165 {
166 template <typename CharType>
167 void CopyCharactersToBuffer(CharType const& value, int32 chunks, int32& position, TArray<CharType>& buffer)
168 {
169 if (buffer.Num() == position) buffer.AddZeroed(chunks);
170 buffer[position] = value;
171 ++position;
172 }
173
174 template <CStringOrView String>
175 void CopyStringToBufferUnsafe(String const& value, int32& position, TArray<TCHAR>& buffer)
176 {
177 FMemory::Memcpy(buffer.GetData() + position, GetData(value), value.Len() * sizeof(TCHAR));
178 position += value.Len();
179 }
180
181 template <CStringOrView String>
182 void CopyStringItemToBuffer(String const& value, FString const& separator, int32 chunks, int32& position, TArray<TCHAR>& buffer)
183 {
184 int nextLength = value.Len() + separator.Len();
185
186 if (buffer.Num() <= position + nextLength) buffer.AddZeroed(chunks);
187 if (!separator.IsEmpty())
188 CopyStringToBufferUnsafe(separator, position, buffer);
189 CopyStringToBufferUnsafe(value, position, buffer);
190 }
191 }
192
193 /**
194 * @brief Render an input range as a string.
195 *
196 * For ranges of any char type, the output is an uninterrupted string of them. Other char types than TCHAR will
197 * be converted to the encoding of the current TCHAR.
198 *
199 * For ranges of strings and string-views individual items will be directly copy-appended to the output separated
200 * by `, ` (unless another separator sequence is set via `Separator`)
201 *
202 * For anything else, `Mcro::Text::AsString` is used. In fact this function serves as the basis for `AsString` for
203 * any range type. Like for strings, any other type is separated by `, ` (unless another separator sequence is set
204 * via `Separator`). For convenience a piped version is also provided of this function.
205 */
206 template <CRangeMember Range>
207 FString RenderAsString(Range&& range)
208 {
209 using ElementType = TRangeElementType<Range>;
210
211 if (IteratorEquals(range.begin(), range.end()))
212 return {};
213
214 constexpr int chunks = 16384;
215 int32 position = 0;
216 FRangeStringFormatOptions rangeFormatOptions;
217
218 if constexpr (CMatchTemplate<Range, Detail::TRangeWithStringFormat>)
219 rangeFormatOptions = range.Options;
220
221 if constexpr (CChar<ElementType>)
222 {
223 TArray<ElementType> buffer;
224 for (ElementType const& character : range)
225 Detail::CopyCharactersToBuffer(character, chunks, position, buffer);
226
227 if constexpr (CCurrentChar<ElementType>)
228 return Mcro::Text::Detail::MakeStringFromPtrSize(buffer.GetData(), position);
229 else
230 {
231 TStdStringView<ElementType> stringView(buffer.GetData(), position);
232 return UnrealConvert(stringView);
233 }
234 }
235 else if constexpr (CStringOrView<ElementType>)
236 {
237 TArray<TCHAR> buffer;
238 for (auto it = range.begin(); !IteratorEquals(it, range.end()); ++it)
239 {
240 ElementType const& value = *it;
241 bool isFirst = IteratorEquals(it, range.begin());
243 value,
244 isFirst ? FString() : rangeFormatOptions.Separator,
245 chunks,
246 position, buffer
247 );
248 }
249 FString output = Mcro::Text::Detail::MakeStringFromPtrSize(buffer.GetData(), position);
250 return rangeFormatOptions.Start + output + rangeFormatOptions.End;
251 }
252 else
253 {
254 TArray<TCHAR> buffer;
255 for (auto it = range.begin(); !IteratorEquals(it, range.end()); ++it)
256 {
257 ElementType const& value = *it;
258 FString valueString = AsString(value);
259
260 bool isFirst = IteratorEquals(it, range.begin());
262 valueString,
263 isFirst ? FString() : rangeFormatOptions.Separator,
264 chunks,
265 position, buffer
266 );
267 }
268 FString output = Mcro::Text::Detail::MakeStringFromPtrSize(buffer.GetData(), position);
269 return rangeFormatOptions.Start + output + rangeFormatOptions.End;
270 }
271 }
272
273 FORCEINLINE auto RenderAsString()
274 {
275 return ranges::make_pipeable([]<CRangeMember Input>(Input&& range)
276 {
277 return RenderAsString(FWD(range));
278 });
279 }
280
281 /**
282 * @brief Render a range as the given container.
283 *
284 * This functor will iterate over the entire input range and copy its values to the newly created container
285 * one-by-one with its `Add` function. If you want a more optimised way to do that use `OutputTo` where you can
286 * supply your own container as an l-value.
287 *
288 * usage:
289 * @code
290 * using namespace ranges;
291 * auto result = views::ints(0)
292 * | views::stride(2)
293 * | views::take(5)
294 * | RenderAs<TArray>();
295 *
296 * // -> TArray<int32> {0, 2, 4, 6, 8}
297 * @endcode
298 *
299 * @tparam Target
300 * An Unreal container template which has a public function member `Add`, the element-type of which will be deduced
301 * from the input left side range.
302 */
303 template <template <typename> typename Target>
305 {
306 template <CRangeMember From, typename Value = TRangeElementType<From>>
308 Target<Value> Convert(From&& range) const
309 {
310 Target<Value> result;
311 for (Value const& value : range)
312 result.Add(value);
313 return result;
314 }
315
316 public:
318
319 template <CRangeMember From>
320 friend auto operator | (From&& range, RenderAs&& functor)
321 {
322 return functor.Convert(FWD(range));
323 }
324
325 template <CRangeMember From>
326 auto Render(From&& range) const
327 {
328 return Convert(FWD(range));
329 }
330 };
331
332 /**
333 * @brief Render a range to an already existing container.
334 *
335 * This functor will iterate over the entire input range and copy its values to the given container one-by-one.
336 * Target container must expose iterators which allows modifying its content. If the input range has more items
337 * than the target container current size, then start using its `Add` function.
338 *
339 * usage:
340 * @code
341 * using namespace ranges;
342 * TArray<int32> Storage;
343 * Storage.SetNumUninitialized(5);
344 *
345 * auto result = views::ints(0, 10)
346 * | views::stride(2)
347 * | OutputTo(Storage);
348 *
349 * // -> TArray<int32> {0, 2, 4, 6, 8}
350 * @endcode
351 *
352 * @tparam Target
353 * An Unreal container which has a public function member `Add`, the element-type of which will be deduced from
354 * the target output container.
355 */
356 template <CUnrealRange Target>
358 {
359 using ElementType = TRangeElementType<Target>;
360 Target& Storage;
361
362 template <CRangeMember From, CConvertibleToDecayed<ElementType> Value = TRangeElementType<From>>
363 void Convert(From&& range)
364 {
365 auto it = Storage.begin();
366 auto endIt = Storage.end();
367 for (Value const& value : range)
368 {
369 if (IteratorEquals(it, endIt))
370 Storage.Add(value);
371 else
372 {
373 *it = value;
374 ++it;
375 }
376 }
377 }
378
379 public:
380 OutputTo(Target& target) : Storage(target) {}
381
382 template <CRangeMember From>
383 friend Target& operator | (From&& range, OutputTo&& functor)
384 {
385 functor.Convert(FWD(range));
386 return functor.Storage;
387 }
388 };
389
390 /**
391 * @brief
392 * Render a range of tuples or range of ranges with at least 2 elements as a TMap.
393 *
394 * This functor will iterate over the entire input range and copy its values to the newly created container
395 * one-by-one with its `Add` function.
396 *
397 * When working with range-of-ranges then ranges which doesn't have at least two elements will be silently ignored.
398 *
399 * usage (from tuples):
400 * @code
401 * using namespace ranges;
402 * TArray<int32> MyKeyArray {1, 2, 3, 4, 5};
403 * TArray<FString> MyValueArray {TEXT_"foo", TEXT_"bar"};
404 *
405 * auto result = views::zip(MyKeyArray, views::cycle(MyValueArray))
406 * | RenderAsMap();
407 *
408 * // -> TMap<int32, FString> {{1, "foo"}, {2, "bar"}, {3, "foo"}, {4, "bar"}, {5, "foo"}}
409 * @endcode
410 *
411 * usage (from inner-ranges):
412 * @code
413 * using namespace ranges;
414 * auto result = views::ints(0, 9)
415 * | views::chunk(2)
416 * | RenderAsMap();
417 *
418 * // -> TMap<int32, int32> {{0, 1}, {2, 3}, {4, 5}, {6, 7}}
419 * // notice how 8 is discarded from the end, as that range didn't have 2 items
420 * @endcode
421 */
423 {
424 template <
425 CRangeMember From,
426 CTuple Value = TRangeElementType<From>,
427 typename MapType = TMap<TTypeAtDecayed<0, Value>, TTypeAtDecayed<1, Value>>
428 >
429 requires (GetSize<Value>() >= 2)
430 static void Convert(From&& range, MapType& result)
431 {
432 for (Value const& value : range)
433 result.Add(GetItem<0>(value), GetItem<1>(value));
434 }
435
436 template <
437 CRangeMember From,
438 CRangeMember InnerRange = TRangeElementType<From>,
439 typename Value = TRangeElementType<InnerRange>,
440 typename MapType = TMap<Value, Value>
441 >
442 static void Convert(From&& range, MapType& result)
443 {
444 for (InnerRange const& innerRange : range)
445 {
446 // TODO: support TMultiMap
447 auto it = innerRange.begin();
448 if (IteratorEquals(it, innerRange.end())) continue;
449 Value const& key = *it;
450 ++it;
451 if (IteratorEquals(it, innerRange.end())) continue;
452 Value const& value = *it;
453 result.Add(key, value);
454 }
455 }
456
457 template <
458 CRangeMember From,
459 CTuple Value = TRangeElementType<From>,
460 typename MapType = TMap<TTypeAtDecayed<0, Value>, TTypeAtDecayed<1, Value>>
461 >
462 requires (GetSize<Value>() >= 2)
463 MapType Convert(From&& range) const
464 {
465 MapType result;
466 Convert(FWD(range), result);
467 return result;
468 }
469
470 template <
471 CRangeMember From,
472 CRangeMember InnerRange = TRangeElementType<From>,
473 typename Value = TRangeElementType<InnerRange>,
474 typename MapType = TMap<Value, Value>
475 >
476 MapType Convert(From&& range) const
477 {
478 MapType result;
479 Convert(FWD(range), result);
480 return result;
481 }
482
483 public:
484 template <CMatchTemplate<TMap> Target>
485 friend class OutputToMap;
486
488
489 template <CRangeMember From>
490 friend auto operator | (From&& range, RenderAsMap&& functor)
491 {
492 return functor.Convert(FWD(range));
493 }
494
495 template <CRangeMember From>
496 auto Render(From&& range) const
497 {
498 return Convert(FWD(range));
499 }
500 };
501
502 /**
503 * @brief
504 * Output a range of tuples or range of ranges with at least 2 elements to an already existing TMap.
505 *
506 * This functor will iterate over the entire input range and copy its values to the existing TMap one-by-one with
507 * its `Add` function.
508 *
509 * When working with range-of-ranges then ranges which doesn't have at least two elements will be silently ignored.
510 *
511 * usage (from tuples):
512 * @code
513 * using namespace ranges;
514 * TArray<int32> MyKeyArray {1, 2, 3, 4, 5};
515 * TArray<FString> MyValueArray {TEXT_"foo", TEXT_"bar"};
516 *
517 * auto result = views::zip(MyKeyArray, views::cycle(MyValueArray))
518 * | RenderAsMap();
519 *
520 * // -> TMap<int32, FString> {{1, "foo"}, {2, "bar"}, {3, "foo"}, {4, "bar"}, {5, "foo"}}
521 * @endcode
522 *
523 * usage (from inner-ranges):
524 * @code
525 * using namespace ranges;
526 * auto result = views::ints(0, 9)
527 * | views::chunk(2)
528 * | RenderAsMap();
529 *
530 * // -> TMap<int32, int32> {{0, 1}, {2, 3}, {4, 5}, {6, 7}}
531 * // notice how 8 is discarded from the end, as that range didn't have 2 items
532 * @endcode
533 *
534 * @tparam Target A TMap. Its key-value types will be deduced from the target output map.
535 */
536 template <CMatchTemplate<TMap> Target>
538 {
539 using KeyType = typename Target::KeyType;
540 using ValueType = typename Target::ValueType;
541 Target& Storage;
542
543 public:
544 OutputToMap(Target& target) : Storage(target) {};
545
546 template <
547 CRangeMember From,
548 CTupleConvertsToArgs<KeyType, ValueType> = TRangeElementType<From>
549 >
550 friend Target& operator | (From&& range, OutputToMap&& functor)
551 {
552 RenderAsMap::Convert(FWD(range), functor.Storage);
553 return functor.Storage;
554 }
555 };
556}
557
558namespace Mcro::Text
559{
560 using namespace Mcro::Range;
561
562 template <CRangeMember Operand>
563 requires (
564 !CDirectStringFormatArgument<Operand>
565 && !CHasToString<Operand>
566 )
568 {
569 template <CConvertibleToDecayed<Operand> Arg>
570 FString operator () (Arg&& left) const { return RenderAsString(left); }
571 };
572}
This header exists because STL headers in Android doesn't define STL concepts (other than same_as whi...
Use this header and Start.h in tandem to include third-party library headers which may not tolerate U...
#define FWD(...)
Shorten forwarding expression with this macro so one may not need to specify explicit type.
Definition Macros.h:134
auto end(TArray< T, A > &r) -> T *
Definition Range.h:99
Use this header and End.h in tandem to include third-party library headers which may not tolerate Unr...
#define TEXT_
A convenience alternative to Unreal's own TEXT macro but this one doesn't require parenthesis around ...
Definition TextMacros.h:53
Output a range of tuples or range of ranges with at least 2 elements to an already existing TMap.
Definition Conversion.h:538
friend Target & operator|(From &&range, OutputToMap &&functor)
Definition Conversion.h:550
OutputToMap(Target &target)
Definition Conversion.h:544
Render a range to an already existing container.
Definition Conversion.h:358
OutputTo(Target &target)
Definition Conversion.h:380
friend Target & operator|(From &&range, OutputTo &&functor)
Definition Conversion.h:383
Render a range of tuples or range of ranges with at least 2 elements as a TMap.
Definition Conversion.h:423
auto Render(From &&range) const
Definition Conversion.h:496
friend auto operator|(From &&range, RenderAsMap &&functor)
Definition Conversion.h:490
Render a range as the given container.
Definition Conversion.h:305
auto Render(From &&range) const
Definition Conversion.h:326
friend auto operator|(From &&range, RenderAs &&functor)
Definition Conversion.h:320
void CopyCharactersToBuffer(CharType const &value, int32 chunks, int32 &position, TArray< CharType > &buffer)
Definition Conversion.h:167
void CopyStringItemToBuffer(String const &value, FString const &separator, int32 chunks, int32 &position, TArray< TCHAR > &buffer)
Definition Conversion.h:182
void CopyStringToBufferUnsafe(String const &value, int32 &position, TArray< TCHAR > &buffer)
Definition Conversion.h:175
Restore range-v3 compatibility for TArray on UE 5.8+.
Definition Concepts.h:20
FORCEINLINE auto NoSeparator()
Don't use a separator when this range is rendered to a string.
Definition Conversion.h:118
FORCEINLINE auto NoEnclosure()
Don't enclose this range in anything when it's rendered to a string.
Definition Conversion.h:132
FORCEINLINE auto Separator(FString const &separator)
Specify a separator sequence for a range when converting it to a string.
Definition Conversion.h:89
FORCEINLINE auto RenderAsString()
Definition Conversion.h:273
bool IteratorEquals(L const &l, R const &r)
Definition Concepts.h:45
FORCEINLINE auto Enclosure(FString const &start, FString const &end)
Specify a start and an end sequence enclosing this range when converting it to a string.
Definition Conversion.h:103
FORCEINLINE auto NoDecorators()
Don't insert anything else than the contents of the input range when that is rendered as a string jus...
Definition Conversion.h:151
TIteratorElementType< decltype(DeclVal< T >().begin())> TRangeElementType
return a range's associated content type determined by dereferencing their iterator.
Definition Concepts.h:161
This namespace provides templating utilities and introspection into template instantiations.
Definition Templates.h:22
FORCEINLINE FString MakeStringFromPtrSize(const TCHAR *ptr, int32 size)
Definition Text.h:135
Mixed text utilities and type traits.
Definition Enums.h:51
FString UnrealConvert(T const &stdStr)
Create a copy and convert an input STL string to TCHAR.
Definition Text.h:199
FString AsString(T &&input)
Attempt to convert anything to string which can tell via some method how to do so.
Definition Text.h:397
std::basic_string_view< CharT, Traits > TStdStringView
Unreal style alias for STL string views.
Definition Text.h:51
decltype(auto) GetItem(T &&tuple)
Definition Tuples.h:106
std::decay_t< typename TTypeAt_Struct< I, T >::Type > TTypeAtDecayed
Definition Tuples.h:157
consteval size_t GetSize()
Definition Tuples.h:124
TRangeWithStringFormat(Range const &range, FRangeStringFormatOptions const &options)
Definition Conversion.h:74