/* Copyright (c) 2005-2021 Intel Corporation Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ #include #include #include #include #include #include #include "oneapi/tbb/concurrent_queue.h" #include "oneapi/tbb/cache_aligned_allocator.h" #include #include //! \file conformance_concurrent_queue.cpp //! \brief Test for [containers.concurrent_queue containers.concurrent_bounded_queue] specification template using test_allocator = StaticSharedCountingAllocator>; static constexpr std::size_t MinThread = 1; static constexpr std::size_t MaxThread = 4; static constexpr std::size_t MAXTHREAD = 256; static constexpr std::size_t M = 10000; static std::atomic PopKind[3]; static int Sum[MAXTHREAD]; template void push(CQ& q, ValueType v, CounterType i) { switch (i % 3) { case 0: q.push( v); break; case 1: q.push( std::move(v)); break; case 2: q.emplace( v); break; default: CHECK(false); break; } } template class ConcQWithCapacity : public oneapi::tbb::concurrent_queue> { using base_type = oneapi::tbb::concurrent_queue>; public: ConcQWithCapacity() : my_capacity( std::size_t(-1) / (sizeof(void*) + sizeof(T)) ) {} std::size_t size() const { return this->unsafe_size(); } std::size_t capacity() const { return my_capacity; } void set_capacity( const std::size_t n ) { my_capacity = n; } bool try_push( const T& source ) { base_type::push( source); return source.get_serial() < my_capacity; } bool try_pop( T& dest ) { base_type::try_pop( dest); return dest.get_serial() < my_capacity; } private: std::size_t my_capacity; }; template void TestEmptyQueue() { const CQ queue; CHECK(queue.size() == 0); CHECK(queue.capacity()> 0); CHECK(size_t(queue.capacity())>= std::size_t(-1)/(sizeof(void*)+sizeof(T))); } void TestEmptiness() { TestEmptyQueue, char>(); TestEmptyQueue, move_support_tests::Foo>(); TestEmptyQueue>, char>(); TestEmptyQueue>, move_support_tests::Foo>(); } template void TestFullQueue() { using allocator_type = decltype(std::declval().get_allocator()); for (std::size_t n = 0; n < 100; ++n) { allocator_type::init_counters(); { CQ queue; queue.set_capacity(n); for (std::size_t i = 0; i <= n; ++i) { T f; f.set_serial(i); bool result = queue.try_push( f); CHECK((result == (i < n))); } for (std::size_t i = 0; i <= n; ++i) { T f; bool result = queue.try_pop(f); CHECK((result == (i < n))); CHECK((result == 0 || f.get_serial() == i)); } } CHECK(allocator_type::items_allocated == allocator_type::items_freed); CHECK(allocator_type::allocations == allocator_type::frees); } } void TestFullness() { TestFullQueue, move_support_tests::Foo>(); TestFullQueue>, move_support_tests::Foo>(); } template void TestClear() { using allocator_type = decltype(std::declval().get_allocator()); allocator_type::init_counters(); const std::size_t n = 5; CQ queue; const std::size_t q_capacity = 10; queue.set_capacity(q_capacity); for (std::size_t i = 0; i < n; ++i) { move_support_tests::Foo f; f.set_serial(i); queue.push(f); } CHECK(queue.size() == n); queue.clear(); CHECK(queue.size()==0); for (std::size_t i = 0; i < n; ++i) { move_support_tests::Foo f; f.set_serial(i); queue.push( f); } CHECK(queue.size() == n); queue.clear(); CHECK(queue.size() == 0); for (std::size_t i = 0; i < n; ++i) { move_support_tests::Foo f; f.set_serial(i); queue.push(f); } CHECK(queue.size()==n); } void TestClearWorks() { TestClear>(); TestClear>>(); } template void TestIteratorAux( Iterator1 i, Iterator2 j, int size ) { Iterator1 old_i; // assigned at first iteration below for (std::size_t k = 0; k < (std::size_t)size; ++k) { CHECK_FAST(i != j); CHECK_FAST(!(i == j)); // Test "->" CHECK_FAST((k+1 == i->get_serial())); if (k & 1) { // Test post-increment move_support_tests::Foo f = *old_i++; CHECK_FAST((k + 1 == f.get_serial())); // Test assignment i = old_i; } else { // Test pre-increment if (k < std::size_t(size - 1)) { move_support_tests::Foo f = *++i; CHECK_FAST((k + 2 == f.get_serial())); } else ++i; // Test assignment old_i = i; } } CHECK_FAST(!(i != j)); CHECK_FAST(i == j); } template void TestIteratorAssignment( Iterator2 j ) { Iterator1 i(j); CHECK(i == j); CHECK(!(i != j)); Iterator1 k; k = j; CHECK(k == j); CHECK(!(k != j)); } template void TestIteratorTraits() { static_assert( std::is_same::value, "wrong iterator category"); T x; typename Iterator::reference xr = x; typename Iterator::pointer xp = &x; CHECK((&xr == xp)); } // Test the iterators for concurrent_queue template void TestIterator() { CQ queue; const CQ& const_queue = queue; for (int j=0; j < 500; ++j) { TestIteratorAux( queue.unsafe_begin() , queue.unsafe_end() , j); TestIteratorAux( queue.unsafe_cbegin() , queue.unsafe_cend() , j); TestIteratorAux( const_queue.unsafe_begin(), const_queue.unsafe_end(), j); TestIteratorAux( const_queue.unsafe_begin(), queue.unsafe_end() , j); TestIteratorAux( queue.unsafe_begin() , const_queue.unsafe_end(), j); move_support_tests::Foo f; f.set_serial(j+1); queue.push(f); } TestIteratorAssignment( const_queue.unsafe_begin()); TestIteratorAssignment( queue.unsafe_begin()); TestIteratorAssignment( queue.unsafe_begin()); TestIteratorTraits(); TestIteratorTraits(); } void TestQueueIteratorWorks() { TestIterator>>(); TestIterator>>(); } // Define wrapper classes to test oneapi::tbb::concurrent_queue template> class ConcQWithSizeWrapper : public oneapi::tbb::concurrent_queue { public: ConcQWithSizeWrapper() {} ConcQWithSizeWrapper( const ConcQWithSizeWrapper& q ) : oneapi::tbb::concurrent_queue(q) {} ConcQWithSizeWrapper( const ConcQWithSizeWrapper& q, const A& a ) : oneapi::tbb::concurrent_queue(q, a) {} ConcQWithSizeWrapper( const A& a ) : oneapi::tbb::concurrent_queue( a ) {} ConcQWithSizeWrapper( ConcQWithSizeWrapper&& q ) : oneapi::tbb::concurrent_queue(std::move(q)) {} ConcQWithSizeWrapper( ConcQWithSizeWrapper&& q, const A& a ) : oneapi::tbb::concurrent_queue(std::move(q), a) { } template ConcQWithSizeWrapper( InputIterator begin, InputIterator end, const A& a = A() ) : oneapi::tbb::concurrent_queue(begin, end, a) {} typename oneapi::tbb::concurrent_queue::size_type size() const { return this->unsafe_size(); } }; enum state_type { LIVE = 0x1234, DEAD = 0xDEAD }; class Bar { state_type state; public: static std::size_t construction_num, destruction_num; std::ptrdiff_t my_id; Bar() : state(LIVE), my_id(-1) {} Bar( std::size_t _i ) : state(LIVE), my_id(_i) { construction_num++; } Bar( const Bar& a_bar ) : state(LIVE) { CHECK_FAST(a_bar.state == LIVE); my_id = a_bar.my_id; construction_num++; } ~Bar() { CHECK_FAST(state == LIVE); state = DEAD; my_id = DEAD; destruction_num++; } void operator=( const Bar& a_bar ) { CHECK_FAST(a_bar.state == LIVE); CHECK_FAST(state == LIVE); my_id = a_bar.my_id; } friend bool operator==( const Bar& bar1, const Bar& bar2 ) ; }; std::size_t Bar::construction_num = 0; std::size_t Bar::destruction_num = 0; bool operator==( const Bar& bar1, const Bar& bar2 ) { CHECK_FAST(bar1.state == LIVE); CHECK_FAST(bar2.state == LIVE); return bar1.my_id == bar2.my_id; } class BarIterator { Bar* bar_ptr; BarIterator(Bar* bp_) : bar_ptr(bp_) {} public: Bar& operator*() const { return *bar_ptr; } BarIterator& operator++() { ++bar_ptr; return *this; } Bar* operator++(int) { Bar* result = &operator*(); operator++(); return result; } friend bool operator==(const BarIterator& bia, const BarIterator& bib) ; friend bool operator!=(const BarIterator& bia, const BarIterator& bib) ; template friend void TestConstructors (); } ; bool operator==(const BarIterator& bia, const BarIterator& bib) { return bia.bar_ptr==bib.bar_ptr; } bool operator!=(const BarIterator& bia, const BarIterator& bib) { return bia.bar_ptr!=bib.bar_ptr; } class Bar_exception : public std::bad_alloc { public: virtual const char *what() const noexcept override { return "making the entry invalid"; } virtual ~Bar_exception() noexcept {} }; class BarEx { static int count; public: state_type state; typedef enum { PREPARATION, COPY_CONSTRUCT } mode_type; static mode_type mode; std::ptrdiff_t my_id; std::ptrdiff_t my_tilda_id; static int button; BarEx() : state(LIVE), my_id(-1), my_tilda_id(-1) {} BarEx(std::size_t _i) : state(LIVE), my_id(_i), my_tilda_id(my_id^(-1)) {} BarEx( const BarEx& a_bar ) : state(LIVE) { CHECK_FAST(a_bar.state == LIVE); my_id = a_bar.my_id; if (mode == PREPARATION) if (!(++count % 100)) { TBB_TEST_THROW(Bar_exception()); } my_tilda_id = a_bar.my_tilda_id; } ~BarEx() { CHECK_FAST(state == LIVE); state = DEAD; my_id = DEAD; } static void set_mode( mode_type m ) { mode = m; } void operator=( const BarEx& a_bar ) { CHECK_FAST(a_bar.state == LIVE); CHECK_FAST(state == LIVE); my_id = a_bar.my_id; my_tilda_id = a_bar.my_tilda_id; } friend bool operator==(const BarEx& bar1, const BarEx& bar2 ) ; }; int BarEx::count = 0; BarEx::mode_type BarEx::mode = BarEx::PREPARATION; bool operator==(const BarEx& bar1, const BarEx& bar2) { CHECK_FAST(bar1.state == LIVE); CHECK_FAST(bar2.state == LIVE); CHECK_FAST((bar1.my_id ^ bar1.my_tilda_id) == -1); CHECK_FAST((bar2.my_id ^ bar2.my_tilda_id) == -1); return bar1.my_id == bar2.my_id && bar1.my_tilda_id == bar2.my_tilda_id; } template void TestConstructors () { CQ src_queue; typename CQ::const_iterator dqb; typename CQ::const_iterator dqe; typename CQ::const_iterator iter; using size_type = typename CQ::size_type; for (size_type size = 0; size < 1001; ++size) { for (size_type i = 0; i < size; ++i) src_queue.push(T(i + (i ^ size))); typename CQ::const_iterator sqb( src_queue.unsafe_begin()); typename CQ::const_iterator sqe( src_queue.unsafe_end() ); CQ dst_queue(sqb, sqe); CQ copy_with_alloc(src_queue, typename CQ::allocator_type()); CHECK_FAST_MESSAGE(src_queue.size() == dst_queue.size(), "different size"); CHECK_FAST_MESSAGE(src_queue.size() == copy_with_alloc.size(), "different size"); src_queue.clear(); } T bar_array[1001]; for (size_type size=0; size < 1001; ++size) { for (size_type i=0; i < size; ++i) { bar_array[i] = T(i+(i^size)); } const TIter sab(bar_array + 0); const TIter sae(bar_array + size); CQ dst_queue2(sab, sae); CHECK_FAST(size == dst_queue2.size()); CHECK_FAST(sab == TIter(bar_array+0)); CHECK_FAST(sae == TIter(bar_array+size)); dqb = dst_queue2.unsafe_begin(); dqe = dst_queue2.unsafe_end(); auto res = std::mismatch(dqb, dqe, bar_array); CHECK_FAST_MESSAGE(res.first == dqe, "unexpected element"); CHECK_FAST_MESSAGE(res.second == bar_array + size, "different size?"); } src_queue.clear(); CQ dst_queue3(src_queue); CHECK(src_queue.size() == dst_queue3.size()); CHECK(0 == dst_queue3.size()); int k = 0; for (size_type i = 0; i < 1001; ++i) { T tmp_bar; src_queue.push(T(++k)); src_queue.push(T(++k)); src_queue.try_pop(tmp_bar); CQ dst_queue4( src_queue); CHECK_FAST(src_queue.size() == dst_queue4.size()); dqb = dst_queue4.unsafe_begin(); dqe = dst_queue4.unsafe_end(); iter = src_queue.unsafe_begin(); auto res = std::mismatch(dqb, dqe, iter); CHECK_FAST_MESSAGE(res.first == dqe, "unexpected element"); CHECK_FAST_MESSAGE(res.second == src_queue.unsafe_end(), "different size?"); } CQ dst_queue5(src_queue); CHECK(src_queue.size() == dst_queue5.size()); dqb = dst_queue5.unsafe_begin(); dqe = dst_queue5.unsafe_end(); iter = src_queue.unsafe_begin(); REQUIRE_MESSAGE(std::equal(dqb, dqe, iter), "unexpected element"); for (size_type i=0; i<100; ++i) { T tmp_bar; src_queue.push(T(i + 1000)); src_queue.push(T(i + 1000)); src_queue.try_pop(tmp_bar); dst_queue5.push(T(i + 1000)); dst_queue5.push(T(i + 1000)); dst_queue5.try_pop(tmp_bar); } CHECK(src_queue.size() == dst_queue5.size()); dqb = dst_queue5.unsafe_begin(); dqe = dst_queue5.unsafe_end(); iter = src_queue.unsafe_begin(); auto res = std::mismatch(dqb, dqe, iter); REQUIRE_MESSAGE(res.first == dqe, "unexpected element"); REQUIRE_MESSAGE(res.second == src_queue.unsafe_end(), "different size?"); #if TBB_USE_EXCEPTIONS k = 0; typename CQ_EX::size_type n_elements = 0; CQ_EX src_queue_ex; for (size_type size = 0; size < 1001; ++size) { T_EX tmp_bar_ex; typename CQ_EX::size_type n_successful_pushes = 0; T_EX::set_mode(T_EX::PREPARATION); try { src_queue_ex.push(T_EX(k + (k ^ size))); ++n_successful_pushes; } catch (...) { } ++k; try { src_queue_ex.push(T_EX(k + (k ^ size))); ++n_successful_pushes; } catch (...) { } ++k; src_queue_ex.try_pop(tmp_bar_ex); n_elements += (n_successful_pushes - 1); CHECK_FAST(src_queue_ex.size() == n_elements); T_EX::set_mode(T_EX::COPY_CONSTRUCT); CQ_EX dst_queue_ex(src_queue_ex); CHECK_FAST(src_queue_ex.size() == dst_queue_ex.size()); typename CQ_EX::const_iterator dqb_ex = dst_queue_ex.unsafe_begin(); typename CQ_EX::const_iterator dqe_ex = dst_queue_ex.unsafe_end(); typename CQ_EX::const_iterator iter_ex = src_queue_ex.unsafe_begin(); auto res2 = std::mismatch(dqb_ex, dqe_ex, iter_ex); CHECK_FAST_MESSAGE(res2.first == dqe_ex, "unexpected element"); CHECK_FAST_MESSAGE(res2.second == src_queue_ex.unsafe_end(), "different size?"); } #endif src_queue.clear(); for (size_type size = 0; size < 1001; ++size) { for (size_type i = 0; i < size; ++i) { src_queue.push(T(i + (i ^ size))); } std::vector locations(size); typename CQ::const_iterator qit = src_queue.unsafe_begin(); for (size_type i = 0; i < size; ++i, ++qit) { locations[i] = &(*qit); } size_type size_of_queue = src_queue.size(); CQ dst_queue(std::move(src_queue)); CHECK_FAST_MESSAGE((src_queue.empty() && src_queue.size() == 0), "not working move constructor?"); CHECK_FAST_MESSAGE((size == size_of_queue && size_of_queue == dst_queue.size()), "not working move constructor?"); CHECK_FAST_MESSAGE( std::equal(locations.begin(), locations.end(), dst_queue.unsafe_begin(), [](const T* t1, const T& r2) { return t1 == &r2; }), "there was data movement during move constructor" ); for (size_type i = 0; i < size; ++i) { T test(i + (i ^ size)); T popped; bool pop_result = dst_queue.try_pop( popped); CHECK_FAST(pop_result); CHECK_FAST(test == popped); } } } void TestQueueConstructors() { TestConstructors, Bar, BarIterator, ConcQWithSizeWrapper, BarEx>(); TestConstructors, Bar, BarIterator, oneapi::tbb::concurrent_bounded_queue, BarEx>(); } template struct TestNegativeQueueBody { oneapi::tbb::concurrent_bounded_queue& queue; const std::size_t nthread; TestNegativeQueueBody( oneapi::tbb::concurrent_bounded_queue& q, std::size_t n ) : queue(q), nthread(n) {} void operator()( std::size_t k ) const { if (k == 0) { int number_of_pops = int(nthread) - 1; // Wait for all pops to pend. while (int(queue.size())> -number_of_pops) { utils::yield(); } for (int i = 0; ; ++i) { CHECK(queue.size() == std::size_t(i - number_of_pops)); CHECK((queue.empty() == (queue.size() <= 0))); if (i == number_of_pops) break; // Satisfy another pop queue.push(T()); } } else { // Pop item from queue T item; queue.pop(item); } } }; //! Test a queue with a negative size. template void TestNegativeQueue( std::size_t nthread ) { oneapi::tbb::concurrent_bounded_queue queue; utils::NativeParallelFor( nthread, TestNegativeQueueBody(queue,nthread)); } template class ConcQPushPopWrapper : public oneapi::tbb::concurrent_queue> { public: ConcQPushPopWrapper() : my_capacity(std::size_t(-1) / (sizeof(void*) + sizeof(T))) {} std::size_t size() const { return this->unsafe_size(); } void set_capacity( const ptrdiff_t n ) { my_capacity = n; } bool try_push( const T& source ) { return this->push( source); } bool try_pop( T& dest ) { return this->oneapi::tbb::concurrent_queue>::try_pop(dest); } std::size_t my_capacity; }; template struct Body { CQ* queue; const std::size_t nthread; Body( std::size_t nthread_ ) : nthread(nthread_) {} void operator()( std::size_t thread_id ) const { long pop_kind[3] = {0, 0, 0}; std::size_t serial[MAXTHREAD + 1]; memset(serial, 0, nthread * sizeof(std::size_t)); CHECK(thread_id < nthread); long sum = 0; for (std::size_t j = 0; j < M; ++j) { T f; f.set_thread_id(move_support_tests::serial_dead_state); f.set_serial(move_support_tests::serial_dead_state); bool prepopped = false; if (j & 1) { prepopped = queue->try_pop(f); ++pop_kind[prepopped]; } T g; g.set_thread_id(thread_id); g.set_serial(j + 1); push(*queue, g, j); if (!prepopped) { while(!(queue)->try_pop(f)) utils::yield(); ++pop_kind[2]; } CHECK_FAST(f.get_thread_id() <= nthread); CHECK_FAST_MESSAGE((f.get_thread_id() == nthread || serial[f.get_thread_id()] < f.get_serial()), "partial order violation"); serial[f.get_thread_id()] = f.get_serial(); sum += int(f.get_serial() - 1); } Sum[thread_id] = sum; for (std::size_t k = 0; k < 3; ++k) PopKind[k] += pop_kind[k]; } }; template void TestPushPop(typename CQ::size_type prefill, std::ptrdiff_t capacity, std::size_t nthread ) { using allocator_type = decltype(std::declval().get_allocator()); CHECK(nthread> 0); std::ptrdiff_t signed_prefill = std::ptrdiff_t(prefill); if (signed_prefill + 1>= capacity) { return; } bool success = false; for (std::size_t k=0; k < 3; ++k) { PopKind[k] = 0; } for (std::size_t trial = 0; !success; ++trial) { allocator_type::init_counters(); Body body(nthread); CQ queue; queue.set_capacity(capacity); body.queue = &queue; for (typename CQ::size_type i = 0; i < prefill; ++i) { T f; f.set_thread_id(nthread); f.set_serial(1 + i); push(queue, f, i); CHECK_FAST(queue.size() == i + 1); CHECK_FAST(!queue.empty()); } utils::NativeParallelFor( nthread, body); int sum = 0; for (std::size_t k = 0; k < nthread; ++k) { sum += Sum[k]; } int expected = int( nthread * ((M - 1) * M / 2) + ((prefill - 1) * prefill) / 2); for (int i = int(prefill); --i>=0;) { CHECK_FAST(!queue.empty()); T f; bool result = queue.try_pop(f); CHECK_FAST(result); CHECK_FAST(int(queue.size()) == i); sum += int(f.get_serial()) - 1; } REQUIRE_MESSAGE(queue.empty(), "The queue should be empty"); REQUIRE_MESSAGE(queue.size() == 0, "The queue should have zero size"); if (sum != expected) { REPORT("sum=%d expected=%d\n",sum,expected); } success = true; if (nthread> 1 && prefill == 0) { // Check that pop_if_present got sufficient exercise for (std::size_t k = 0; k < 2; ++k) { const int min_requirement = 100; const int max_trial = 20; if (PopKind[k] < min_requirement) { if (trial>= max_trial) { REPORT("Warning: %d threads had only %ld pop_if_present operations %s after %d trials (expected at least %d). " "This problem may merely be unlucky scheduling. " "Investigate only if it happens repeatedly.\n", nthread, long(PopKind[k]), k==0?"failed":"succeeded", max_trial, min_requirement); } else { success = false; } } } } } } void TestConcurrentPushPop() { for (std::size_t nthread = MinThread; nthread <= MaxThread; ++nthread) { INFO(" Testing with "<< nthread << " thread(s)"); TestNegativeQueue(nthread); for (std::size_t prefill=0; prefill < 64; prefill += (1 + prefill / 3)) { TestPushPop, move_support_tests::Foo>(prefill, std::ptrdiff_t(-1), nthread); TestPushPop, move_support_tests::Foo>(prefill, std::ptrdiff_t(1), nthread); TestPushPop, move_support_tests::Foo>(prefill, std::ptrdiff_t(2), nthread); TestPushPop, move_support_tests::Foo>(prefill, std::ptrdiff_t(10), nthread); TestPushPop, move_support_tests::Foo>(prefill, std::ptrdiff_t(100), nthread); } for (std::size_t prefill = 0; prefill < 64; prefill += (1 + prefill / 3) ) { TestPushPop>, move_support_tests::Foo>(prefill, std::ptrdiff_t(-1), nthread); TestPushPop>, move_support_tests::Foo>(prefill, std::ptrdiff_t(1), nthread); TestPushPop>, move_support_tests::Foo>(prefill, std::ptrdiff_t(2), nthread); TestPushPop>, move_support_tests::Foo>(prefill, std::ptrdiff_t(10), nthread); TestPushPop>, move_support_tests::Foo>(prefill, std::ptrdiff_t(100), nthread); } } } class Foo_exception : public std::bad_alloc { public: virtual const char *what() const throw() override { return "out of Foo limit"; } virtual ~Foo_exception() throw() {} }; #if TBB_USE_EXCEPTIONS static std::atomic FooExConstructed; static std::atomic FooExDestroyed; static std::atomic serial_source; static long MaxFooCount = 0; static const long Threshold = 400; class FooEx { state_type state; public: int serial; FooEx() : state(LIVE) { ++FooExConstructed; serial = serial_source++; } FooEx( const FooEx& item ) : state(LIVE) { CHECK(item.state == LIVE); ++FooExConstructed; if (MaxFooCount && (FooExConstructed - FooExDestroyed) >= MaxFooCount) { // in push() throw Foo_exception(); } serial = item.serial; } ~FooEx() { CHECK(state==LIVE); ++FooExDestroyed; state=DEAD; serial=DEAD; } void operator=( FooEx& item ) { CHECK(item.state==LIVE); CHECK(state==LIVE); serial = item.serial; if( MaxFooCount==2*Threshold && (FooExConstructed-FooExDestroyed) <= MaxFooCount/4 ) // in pop() throw Foo_exception(); } void operator=( FooEx&& item ) { operator=( item ); item.serial = 0; } }; template