1 | //===----------------------------------------------------------------------===// |
2 | // |
3 | // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. |
4 | // See https://llvm.org/LICENSE.txt for license information. |
5 | // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception |
6 | // |
7 | //===----------------------------------------------------------------------===// |
8 | |
9 | // UNSUPPORTED: c++03, c++11, c++14 |
10 | |
11 | // <variant> |
12 | |
13 | // template <class ...Types> class variant; |
14 | |
15 | // constexpr variant& operator=(variant const&); |
16 | |
17 | #include <cassert> |
18 | #include <string> |
19 | #include <type_traits> |
20 | #include <variant> |
21 | |
22 | #include "test_macros.h" |
23 | |
24 | struct NoCopy { |
25 | NoCopy(const NoCopy &) = delete; |
26 | NoCopy &operator=(const NoCopy &) = default; |
27 | }; |
28 | |
29 | struct CopyOnly { |
30 | CopyOnly(const CopyOnly &) = default; |
31 | CopyOnly(CopyOnly &&) = delete; |
32 | CopyOnly &operator=(const CopyOnly &) = default; |
33 | CopyOnly &operator=(CopyOnly &&) = delete; |
34 | }; |
35 | |
36 | struct MoveOnly { |
37 | MoveOnly(const MoveOnly &) = delete; |
38 | MoveOnly(MoveOnly &&) = default; |
39 | MoveOnly &operator=(const MoveOnly &) = default; |
40 | }; |
41 | |
42 | struct MoveOnlyNT { |
43 | MoveOnlyNT(const MoveOnlyNT &) = delete; |
44 | MoveOnlyNT(MoveOnlyNT &&) {} |
45 | MoveOnlyNT &operator=(const MoveOnlyNT &) = default; |
46 | }; |
47 | |
48 | struct CopyAssign { |
49 | static int alive; |
50 | static int copy_construct; |
51 | static int copy_assign; |
52 | static int move_construct; |
53 | static int move_assign; |
54 | static void reset() { |
55 | copy_construct = copy_assign = move_construct = move_assign = alive = 0; |
56 | } |
57 | CopyAssign(int v) : value(v) { ++alive; } |
58 | CopyAssign(const CopyAssign &o) : value(o.value) { |
59 | ++alive; |
60 | ++copy_construct; |
61 | } |
62 | CopyAssign(CopyAssign &&o) noexcept : value(o.value) { |
63 | o.value = -1; |
64 | ++alive; |
65 | ++move_construct; |
66 | } |
67 | CopyAssign &operator=(const CopyAssign &o) { |
68 | value = o.value; |
69 | ++copy_assign; |
70 | return *this; |
71 | } |
72 | CopyAssign &operator=(CopyAssign &&o) noexcept { |
73 | value = o.value; |
74 | o.value = -1; |
75 | ++move_assign; |
76 | return *this; |
77 | } |
78 | ~CopyAssign() { --alive; } |
79 | int value; |
80 | }; |
81 | |
82 | int CopyAssign::alive = 0; |
83 | int CopyAssign::copy_construct = 0; |
84 | int CopyAssign::copy_assign = 0; |
85 | int CopyAssign::move_construct = 0; |
86 | int CopyAssign::move_assign = 0; |
87 | |
88 | struct CopyMaybeThrows { |
89 | CopyMaybeThrows(const CopyMaybeThrows &); |
90 | CopyMaybeThrows &operator=(const CopyMaybeThrows &); |
91 | }; |
92 | struct CopyDoesThrow { |
93 | CopyDoesThrow(const CopyDoesThrow &) noexcept(false); |
94 | CopyDoesThrow &operator=(const CopyDoesThrow &) noexcept(false); |
95 | }; |
96 | |
97 | |
98 | struct NTCopyAssign { |
99 | constexpr NTCopyAssign(int v) : value(v) {} |
100 | NTCopyAssign(const NTCopyAssign &) = default; |
101 | NTCopyAssign(NTCopyAssign &&) = default; |
102 | NTCopyAssign &operator=(const NTCopyAssign &that) { |
103 | value = that.value; |
104 | return *this; |
105 | }; |
106 | NTCopyAssign &operator=(NTCopyAssign &&) = delete; |
107 | int value; |
108 | }; |
109 | |
110 | static_assert(!std::is_trivially_copy_assignable<NTCopyAssign>::value, "" ); |
111 | static_assert(std::is_copy_assignable<NTCopyAssign>::value, "" ); |
112 | |
113 | struct TCopyAssign { |
114 | constexpr TCopyAssign(int v) : value(v) {} |
115 | TCopyAssign(const TCopyAssign &) = default; |
116 | TCopyAssign(TCopyAssign &&) = default; |
117 | TCopyAssign &operator=(const TCopyAssign &) = default; |
118 | TCopyAssign &operator=(TCopyAssign &&) = delete; |
119 | int value; |
120 | }; |
121 | |
122 | static_assert(std::is_trivially_copy_assignable<TCopyAssign>::value, "" ); |
123 | |
124 | struct TCopyAssignNTMoveAssign { |
125 | constexpr TCopyAssignNTMoveAssign(int v) : value(v) {} |
126 | TCopyAssignNTMoveAssign(const TCopyAssignNTMoveAssign &) = default; |
127 | TCopyAssignNTMoveAssign(TCopyAssignNTMoveAssign &&) = default; |
128 | TCopyAssignNTMoveAssign &operator=(const TCopyAssignNTMoveAssign &) = default; |
129 | TCopyAssignNTMoveAssign &operator=(TCopyAssignNTMoveAssign &&that) { |
130 | value = that.value; |
131 | that.value = -1; |
132 | return *this; |
133 | } |
134 | int value; |
135 | }; |
136 | |
137 | static_assert(std::is_trivially_copy_assignable_v<TCopyAssignNTMoveAssign>, "" ); |
138 | |
139 | #ifndef TEST_HAS_NO_EXCEPTIONS |
140 | struct CopyThrows { |
141 | CopyThrows() = default; |
142 | CopyThrows(const CopyThrows &) { throw 42; } |
143 | CopyThrows &operator=(const CopyThrows &) { throw 42; } |
144 | }; |
145 | |
146 | struct CopyCannotThrow { |
147 | static int alive; |
148 | CopyCannotThrow() { ++alive; } |
149 | CopyCannotThrow(const CopyCannotThrow &) noexcept { ++alive; } |
150 | CopyCannotThrow(CopyCannotThrow &&) noexcept { assert(false); } |
151 | CopyCannotThrow &operator=(const CopyCannotThrow &) noexcept = default; |
152 | CopyCannotThrow &operator=(CopyCannotThrow &&) noexcept { assert(false); return *this; } |
153 | }; |
154 | |
155 | int CopyCannotThrow::alive = 0; |
156 | |
157 | struct MoveThrows { |
158 | static int alive; |
159 | MoveThrows() { ++alive; } |
160 | MoveThrows(const MoveThrows &) { ++alive; } |
161 | MoveThrows(MoveThrows &&) { throw 42; } |
162 | MoveThrows &operator=(const MoveThrows &) { return *this; } |
163 | MoveThrows &operator=(MoveThrows &&) { throw 42; } |
164 | ~MoveThrows() { --alive; } |
165 | }; |
166 | |
167 | int MoveThrows::alive = 0; |
168 | |
169 | struct MakeEmptyT { |
170 | static int alive; |
171 | MakeEmptyT() { ++alive; } |
172 | MakeEmptyT(const MakeEmptyT &) { |
173 | ++alive; |
174 | // Don't throw from the copy constructor since variant's assignment |
175 | // operator performs a copy before committing to the assignment. |
176 | } |
177 | MakeEmptyT(MakeEmptyT &&) { throw 42; } |
178 | MakeEmptyT &operator=(const MakeEmptyT &) { throw 42; } |
179 | MakeEmptyT &operator=(MakeEmptyT &&) { throw 42; } |
180 | ~MakeEmptyT() { --alive; } |
181 | }; |
182 | |
183 | int MakeEmptyT::alive = 0; |
184 | |
185 | template <class Variant> void makeEmpty(Variant &v) { |
186 | Variant v2(std::in_place_type<MakeEmptyT>); |
187 | try { |
188 | v = std::move(v2); |
189 | assert(false); |
190 | } catch (...) { |
191 | assert(v.valueless_by_exception()); |
192 | } |
193 | } |
194 | #endif // TEST_HAS_NO_EXCEPTIONS |
195 | |
196 | void test_copy_assignment_not_noexcept() { |
197 | { |
198 | using V = std::variant<CopyMaybeThrows>; |
199 | static_assert(!std::is_nothrow_copy_assignable<V>::value, "" ); |
200 | } |
201 | { |
202 | using V = std::variant<int, CopyDoesThrow>; |
203 | static_assert(!std::is_nothrow_copy_assignable<V>::value, "" ); |
204 | } |
205 | } |
206 | |
207 | void test_copy_assignment_sfinae() { |
208 | { |
209 | using V = std::variant<int, long>; |
210 | static_assert(std::is_copy_assignable<V>::value, "" ); |
211 | } |
212 | { |
213 | using V = std::variant<int, CopyOnly>; |
214 | static_assert(std::is_copy_assignable<V>::value, "" ); |
215 | } |
216 | { |
217 | using V = std::variant<int, NoCopy>; |
218 | static_assert(!std::is_copy_assignable<V>::value, "" ); |
219 | } |
220 | { |
221 | using V = std::variant<int, MoveOnly>; |
222 | static_assert(!std::is_copy_assignable<V>::value, "" ); |
223 | } |
224 | { |
225 | using V = std::variant<int, MoveOnlyNT>; |
226 | static_assert(!std::is_copy_assignable<V>::value, "" ); |
227 | } |
228 | |
229 | // Make sure we properly propagate triviality (see P0602R4). |
230 | { |
231 | using V = std::variant<int, long>; |
232 | static_assert(std::is_trivially_copy_assignable<V>::value, "" ); |
233 | } |
234 | { |
235 | using V = std::variant<int, NTCopyAssign>; |
236 | static_assert(!std::is_trivially_copy_assignable<V>::value, "" ); |
237 | static_assert(std::is_copy_assignable<V>::value, "" ); |
238 | } |
239 | { |
240 | using V = std::variant<int, TCopyAssign>; |
241 | static_assert(std::is_trivially_copy_assignable<V>::value, "" ); |
242 | } |
243 | { |
244 | using V = std::variant<int, TCopyAssignNTMoveAssign>; |
245 | static_assert(std::is_trivially_copy_assignable<V>::value, "" ); |
246 | } |
247 | { |
248 | using V = std::variant<int, CopyOnly>; |
249 | static_assert(std::is_trivially_copy_assignable<V>::value, "" ); |
250 | } |
251 | } |
252 | |
253 | void test_copy_assignment_empty_empty() { |
254 | #ifndef TEST_HAS_NO_EXCEPTIONS |
255 | using MET = MakeEmptyT; |
256 | { |
257 | using V = std::variant<int, long, MET>; |
258 | V v1(std::in_place_index<0>); |
259 | makeEmpty(v&: v1); |
260 | V v2(std::in_place_index<0>); |
261 | makeEmpty(v&: v2); |
262 | V &vref = (v1 = v2); |
263 | assert(&vref == &v1); |
264 | assert(v1.valueless_by_exception()); |
265 | assert(v1.index() == std::variant_npos); |
266 | } |
267 | #endif // TEST_HAS_NO_EXCEPTIONS |
268 | } |
269 | |
270 | void test_copy_assignment_non_empty_empty() { |
271 | #ifndef TEST_HAS_NO_EXCEPTIONS |
272 | using MET = MakeEmptyT; |
273 | { |
274 | using V = std::variant<int, MET>; |
275 | V v1(std::in_place_index<0>, 42); |
276 | V v2(std::in_place_index<0>); |
277 | makeEmpty(v&: v2); |
278 | V &vref = (v1 = v2); |
279 | assert(&vref == &v1); |
280 | assert(v1.valueless_by_exception()); |
281 | assert(v1.index() == std::variant_npos); |
282 | } |
283 | { |
284 | using V = std::variant<int, MET, std::string>; |
285 | V v1(std::in_place_index<2>, "hello" ); |
286 | V v2(std::in_place_index<0>); |
287 | makeEmpty(v&: v2); |
288 | V &vref = (v1 = v2); |
289 | assert(&vref == &v1); |
290 | assert(v1.valueless_by_exception()); |
291 | assert(v1.index() == std::variant_npos); |
292 | } |
293 | #endif // TEST_HAS_NO_EXCEPTIONS |
294 | } |
295 | |
296 | void test_copy_assignment_empty_non_empty() { |
297 | #ifndef TEST_HAS_NO_EXCEPTIONS |
298 | using MET = MakeEmptyT; |
299 | { |
300 | using V = std::variant<int, MET>; |
301 | V v1(std::in_place_index<0>); |
302 | makeEmpty(v&: v1); |
303 | V v2(std::in_place_index<0>, 42); |
304 | V &vref = (v1 = v2); |
305 | assert(&vref == &v1); |
306 | assert(v1.index() == 0); |
307 | assert(std::get<0>(v1) == 42); |
308 | } |
309 | { |
310 | using V = std::variant<int, MET, std::string>; |
311 | V v1(std::in_place_index<0>); |
312 | makeEmpty(v&: v1); |
313 | V v2(std::in_place_type<std::string>, "hello" ); |
314 | V &vref = (v1 = v2); |
315 | assert(&vref == &v1); |
316 | assert(v1.index() == 2); |
317 | assert(std::get<2>(v1) == "hello" ); |
318 | } |
319 | #endif // TEST_HAS_NO_EXCEPTIONS |
320 | } |
321 | |
322 | template <typename T> struct Result { std::size_t index; T value; }; |
323 | |
324 | void test_copy_assignment_same_index() { |
325 | { |
326 | using V = std::variant<int>; |
327 | V v1(43); |
328 | V v2(42); |
329 | V &vref = (v1 = v2); |
330 | assert(&vref == &v1); |
331 | assert(v1.index() == 0); |
332 | assert(std::get<0>(v1) == 42); |
333 | } |
334 | { |
335 | using V = std::variant<int, long, unsigned>; |
336 | V v1(43l); |
337 | V v2(42l); |
338 | V &vref = (v1 = v2); |
339 | assert(&vref == &v1); |
340 | assert(v1.index() == 1); |
341 | assert(std::get<1>(v1) == 42); |
342 | } |
343 | { |
344 | using V = std::variant<int, CopyAssign, unsigned>; |
345 | V v1(std::in_place_type<CopyAssign>, 43); |
346 | V v2(std::in_place_type<CopyAssign>, 42); |
347 | CopyAssign::reset(); |
348 | V &vref = (v1 = v2); |
349 | assert(&vref == &v1); |
350 | assert(v1.index() == 1); |
351 | assert(std::get<1>(v1).value == 42); |
352 | assert(CopyAssign::copy_construct == 0); |
353 | assert(CopyAssign::move_construct == 0); |
354 | assert(CopyAssign::copy_assign == 1); |
355 | } |
356 | #ifndef TEST_HAS_NO_EXCEPTIONS |
357 | using MET = MakeEmptyT; |
358 | { |
359 | using V = std::variant<int, MET, std::string>; |
360 | V v1(std::in_place_type<MET>); |
361 | MET &mref = std::get<1>(v&: v1); |
362 | V v2(std::in_place_type<MET>); |
363 | try { |
364 | v1 = v2; |
365 | assert(false); |
366 | } catch (...) { |
367 | } |
368 | assert(v1.index() == 1); |
369 | assert(&std::get<1>(v1) == &mref); |
370 | } |
371 | #endif // TEST_HAS_NO_EXCEPTIONS |
372 | |
373 | // Make sure we properly propagate triviality, which implies constexpr-ness (see P0602R4). |
374 | { |
375 | struct { |
376 | constexpr Result<int> operator()() const { |
377 | using V = std::variant<int>; |
378 | V v(43); |
379 | V v2(42); |
380 | v = v2; |
381 | return {.index: v.index(), .value: std::get<0>(v&: v)}; |
382 | } |
383 | } test; |
384 | constexpr auto result = test(); |
385 | static_assert(result.index == 0, "" ); |
386 | static_assert(result.value == 42, "" ); |
387 | } |
388 | { |
389 | struct { |
390 | constexpr Result<long> operator()() const { |
391 | using V = std::variant<int, long, unsigned>; |
392 | V v(43l); |
393 | V v2(42l); |
394 | v = v2; |
395 | return {.index: v.index(), .value: std::get<1>(v&: v)}; |
396 | } |
397 | } test; |
398 | constexpr auto result = test(); |
399 | static_assert(result.index == 1, "" ); |
400 | static_assert(result.value == 42l, "" ); |
401 | } |
402 | { |
403 | struct { |
404 | constexpr Result<int> operator()() const { |
405 | using V = std::variant<int, TCopyAssign, unsigned>; |
406 | V v(std::in_place_type<TCopyAssign>, 43); |
407 | V v2(std::in_place_type<TCopyAssign>, 42); |
408 | v = v2; |
409 | return {.index: v.index(), .value: std::get<1>(v&: v).value}; |
410 | } |
411 | } test; |
412 | constexpr auto result = test(); |
413 | static_assert(result.index == 1, "" ); |
414 | static_assert(result.value == 42, "" ); |
415 | } |
416 | { |
417 | struct { |
418 | constexpr Result<int> operator()() const { |
419 | using V = std::variant<int, TCopyAssignNTMoveAssign, unsigned>; |
420 | V v(std::in_place_type<TCopyAssignNTMoveAssign>, 43); |
421 | V v2(std::in_place_type<TCopyAssignNTMoveAssign>, 42); |
422 | v = v2; |
423 | return {.index: v.index(), .value: std::get<1>(v&: v).value}; |
424 | } |
425 | } test; |
426 | constexpr auto result = test(); |
427 | static_assert(result.index == 1, "" ); |
428 | static_assert(result.value == 42, "" ); |
429 | } |
430 | } |
431 | |
432 | void test_copy_assignment_different_index() { |
433 | { |
434 | using V = std::variant<int, long, unsigned>; |
435 | V v1(43); |
436 | V v2(42l); |
437 | V &vref = (v1 = v2); |
438 | assert(&vref == &v1); |
439 | assert(v1.index() == 1); |
440 | assert(std::get<1>(v1) == 42); |
441 | } |
442 | { |
443 | using V = std::variant<int, CopyAssign, unsigned>; |
444 | CopyAssign::reset(); |
445 | V v1(std::in_place_type<unsigned>, 43u); |
446 | V v2(std::in_place_type<CopyAssign>, 42); |
447 | assert(CopyAssign::copy_construct == 0); |
448 | assert(CopyAssign::move_construct == 0); |
449 | assert(CopyAssign::alive == 1); |
450 | V &vref = (v1 = v2); |
451 | assert(&vref == &v1); |
452 | assert(v1.index() == 1); |
453 | assert(std::get<1>(v1).value == 42); |
454 | assert(CopyAssign::alive == 2); |
455 | assert(CopyAssign::copy_construct == 1); |
456 | assert(CopyAssign::move_construct == 1); |
457 | assert(CopyAssign::copy_assign == 0); |
458 | } |
459 | #ifndef TEST_HAS_NO_EXCEPTIONS |
460 | { |
461 | using V = std::variant<int, CopyThrows, std::string>; |
462 | V v1(std::in_place_type<std::string>, "hello" ); |
463 | V v2(std::in_place_type<CopyThrows>); |
464 | try { |
465 | v1 = v2; |
466 | assert(false); |
467 | } catch (...) { /* ... */ |
468 | } |
469 | // Test that copy construction is used directly if move construction may throw, |
470 | // resulting in a valueless variant if copy throws. |
471 | assert(v1.valueless_by_exception()); |
472 | } |
473 | { |
474 | using V = std::variant<int, MoveThrows, std::string>; |
475 | V v1(std::in_place_type<std::string>, "hello" ); |
476 | V v2(std::in_place_type<MoveThrows>); |
477 | assert(MoveThrows::alive == 1); |
478 | // Test that copy construction is used directly if move construction may throw. |
479 | v1 = v2; |
480 | assert(v1.index() == 1); |
481 | assert(v2.index() == 1); |
482 | assert(MoveThrows::alive == 2); |
483 | } |
484 | { |
485 | // Test that direct copy construction is preferred when it cannot throw. |
486 | using V = std::variant<int, CopyCannotThrow, std::string>; |
487 | V v1(std::in_place_type<std::string>, "hello" ); |
488 | V v2(std::in_place_type<CopyCannotThrow>); |
489 | assert(CopyCannotThrow::alive == 1); |
490 | v1 = v2; |
491 | assert(v1.index() == 1); |
492 | assert(v2.index() == 1); |
493 | assert(CopyCannotThrow::alive == 2); |
494 | } |
495 | { |
496 | using V = std::variant<int, CopyThrows, std::string>; |
497 | V v1(std::in_place_type<CopyThrows>); |
498 | V v2(std::in_place_type<std::string>, "hello" ); |
499 | V &vref = (v1 = v2); |
500 | assert(&vref == &v1); |
501 | assert(v1.index() == 2); |
502 | assert(std::get<2>(v1) == "hello" ); |
503 | assert(v2.index() == 2); |
504 | assert(std::get<2>(v2) == "hello" ); |
505 | } |
506 | { |
507 | using V = std::variant<int, MoveThrows, std::string>; |
508 | V v1(std::in_place_type<MoveThrows>); |
509 | V v2(std::in_place_type<std::string>, "hello" ); |
510 | V &vref = (v1 = v2); |
511 | assert(&vref == &v1); |
512 | assert(v1.index() == 2); |
513 | assert(std::get<2>(v1) == "hello" ); |
514 | assert(v2.index() == 2); |
515 | assert(std::get<2>(v2) == "hello" ); |
516 | } |
517 | #endif // TEST_HAS_NO_EXCEPTIONS |
518 | |
519 | // Make sure we properly propagate triviality, which implies constexpr-ness (see P0602R4). |
520 | { |
521 | struct { |
522 | constexpr Result<long> operator()() const { |
523 | using V = std::variant<int, long, unsigned>; |
524 | V v(43); |
525 | V v2(42l); |
526 | v = v2; |
527 | return {.index: v.index(), .value: std::get<1>(v&: v)}; |
528 | } |
529 | } test; |
530 | constexpr auto result = test(); |
531 | static_assert(result.index == 1, "" ); |
532 | static_assert(result.value == 42l, "" ); |
533 | } |
534 | { |
535 | struct { |
536 | constexpr Result<int> operator()() const { |
537 | using V = std::variant<int, TCopyAssign, unsigned>; |
538 | V v(std::in_place_type<unsigned>, 43u); |
539 | V v2(std::in_place_type<TCopyAssign>, 42); |
540 | v = v2; |
541 | return {.index: v.index(), .value: std::get<1>(v&: v).value}; |
542 | } |
543 | } test; |
544 | constexpr auto result = test(); |
545 | static_assert(result.index == 1, "" ); |
546 | static_assert(result.value == 42, "" ); |
547 | } |
548 | } |
549 | |
550 | template <std::size_t NewIdx, class ValueType> |
551 | constexpr bool test_constexpr_assign_imp( |
552 | std::variant<long, void*, int>&& v, ValueType&& new_value) |
553 | { |
554 | const std::variant<long, void*, int> cp( |
555 | std::forward<ValueType>(new_value)); |
556 | v = cp; |
557 | return v.index() == NewIdx && |
558 | std::get<NewIdx>(v) == std::get<NewIdx>(cp); |
559 | } |
560 | |
561 | void test_constexpr_copy_assignment() { |
562 | // Make sure we properly propagate triviality, which implies constexpr-ness (see P0602R4). |
563 | using V = std::variant<long, void*, int>; |
564 | static_assert(std::is_trivially_copyable<V>::value, "" ); |
565 | static_assert(std::is_trivially_copy_assignable<V>::value, "" ); |
566 | static_assert(test_constexpr_assign_imp<0>(v: V(42l), new_value: 101l), "" ); |
567 | static_assert(test_constexpr_assign_imp<0>(v: V(nullptr), new_value: 101l), "" ); |
568 | static_assert(test_constexpr_assign_imp<1>(v: V(42l), new_value: nullptr), "" ); |
569 | static_assert(test_constexpr_assign_imp<2>(v: V(42l), new_value: 101), "" ); |
570 | } |
571 | |
572 | int main(int, char**) { |
573 | test_copy_assignment_empty_empty(); |
574 | test_copy_assignment_non_empty_empty(); |
575 | test_copy_assignment_empty_non_empty(); |
576 | test_copy_assignment_same_index(); |
577 | test_copy_assignment_different_index(); |
578 | test_copy_assignment_sfinae(); |
579 | test_copy_assignment_not_noexcept(); |
580 | test_constexpr_copy_assignment(); |
581 | |
582 | return 0; |
583 | } |
584 | |