| //===-- GenericList.cpp ---------------------------------------------------===// |
| // |
| // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. |
| // See https://llvm.org/LICENSE.txt for license information. |
| // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception |
| // |
| //===----------------------------------------------------------------------===// |
| |
| #include "LibCxx.h" |
| #include "MsvcStl.h" |
| |
| #include "lldb/DataFormatters/FormattersHelpers.h" |
| #include "lldb/Target/Target.h" |
| #include "lldb/Utility/Status.h" |
| #include "lldb/ValueObject/ValueObject.h" |
| #include "lldb/ValueObject/ValueObjectConstResult.h" |
| #include "lldb/lldb-enumerations.h" |
| |
| using namespace lldb; |
| using namespace lldb_private; |
| using namespace lldb_private::formatters; |
| |
| namespace { |
| |
| enum class StlType { |
| LibCxx, |
| MsvcStl, |
| }; |
| |
| template <StlType Stl> class ListEntry { |
| public: |
| ListEntry() = default; |
| ListEntry(ValueObjectSP entry_sp) : m_entry_sp(std::move(entry_sp)) {} |
| ListEntry(ValueObject *entry) |
| : m_entry_sp(entry ? entry->GetSP() : ValueObjectSP()) {} |
| |
| uint64_t value() const { |
| if (!m_entry_sp) |
| return 0; |
| return m_entry_sp->GetValueAsUnsigned(0); |
| } |
| |
| ListEntry next(); |
| ListEntry prev(); |
| |
| bool null() { return (value() == 0); } |
| |
| explicit operator bool() { return GetEntry() && !null(); } |
| |
| ValueObjectSP GetEntry() { return m_entry_sp; } |
| |
| void SetEntry(ValueObjectSP entry) { m_entry_sp = entry; } |
| |
| bool operator==(const ListEntry &rhs) const { return value() == rhs.value(); } |
| |
| bool operator!=(const ListEntry &rhs) const { return !(*this == rhs); } |
| |
| private: |
| ValueObjectSP m_entry_sp; |
| }; |
| |
| template <> ListEntry<StlType::LibCxx> ListEntry<StlType::LibCxx>::next() { |
| if (!m_entry_sp) |
| return ListEntry(); |
| return ListEntry(m_entry_sp->GetChildMemberWithName("__next_")); |
| } |
| |
| template <> ListEntry<StlType::LibCxx> ListEntry<StlType::LibCxx>::prev() { |
| if (!m_entry_sp) |
| return ListEntry(); |
| return ListEntry(m_entry_sp->GetChildMemberWithName("__prev_")); |
| } |
| |
| template <> ListEntry<StlType::MsvcStl> ListEntry<StlType::MsvcStl>::next() { |
| if (!m_entry_sp) |
| return ListEntry(); |
| return ListEntry(m_entry_sp->GetChildMemberWithName("_Next")); |
| } |
| |
| template <> ListEntry<StlType::MsvcStl> ListEntry<StlType::MsvcStl>::prev() { |
| if (!m_entry_sp) |
| return ListEntry(); |
| return ListEntry(m_entry_sp->GetChildMemberWithName("_Prev")); |
| } |
| |
| template <StlType Stl> class ListIterator { |
| public: |
| ListIterator() = default; |
| ListIterator(ListEntry<Stl> entry) : m_entry(std::move(entry)) {} |
| ListIterator(ValueObjectSP entry) : m_entry(std::move(entry)) {} |
| ListIterator(ValueObject *entry) : m_entry(entry) {} |
| |
| ValueObjectSP value() { return m_entry.GetEntry(); } |
| |
| ValueObjectSP advance(size_t count) { |
| if (count == 0) |
| return m_entry.GetEntry(); |
| if (count == 1) { |
| next(); |
| return m_entry.GetEntry(); |
| } |
| while (count > 0) { |
| next(); |
| count--; |
| if (m_entry.null()) |
| return lldb::ValueObjectSP(); |
| } |
| return m_entry.GetEntry(); |
| } |
| |
| bool operator==(const ListIterator &rhs) const { |
| return (rhs.m_entry == m_entry); |
| } |
| |
| protected: |
| void next() { m_entry = m_entry.next(); } |
| |
| void prev() { m_entry = m_entry.prev(); } |
| |
| private: |
| ListEntry<Stl> m_entry; |
| }; |
| |
| template <StlType Stl> |
| class AbstractListFrontEnd : public SyntheticChildrenFrontEnd { |
| public: |
| llvm::Expected<size_t> GetIndexOfChildWithName(ConstString name) override { |
| auto optional_idx = formatters::ExtractIndexFromString(name.GetCString()); |
| if (!optional_idx) { |
| return llvm::createStringError("Type has no child named '%s'", |
| name.AsCString()); |
| } |
| return *optional_idx; |
| } |
| lldb::ChildCacheState Update() override; |
| |
| protected: |
| AbstractListFrontEnd(ValueObject &valobj) |
| : SyntheticChildrenFrontEnd(valobj) {} |
| |
| size_t m_count = 0; |
| ValueObject *m_head = nullptr; |
| |
| static constexpr bool g_use_loop_detect = true; |
| size_t m_loop_detected = 0; // The number of elements that have had loop |
| // detection run over them. |
| ListEntry<Stl> m_slow_runner; // Used for loop detection |
| ListEntry<Stl> m_fast_runner; // Used for loop detection |
| |
| size_t m_list_capping_size = 0; |
| CompilerType m_element_type; |
| std::map<size_t, ListIterator<Stl>> m_iterators; |
| |
| bool HasLoop(size_t count); |
| ValueObjectSP GetItem(size_t idx); |
| }; |
| |
| class LibCxxForwardListFrontEnd : public AbstractListFrontEnd<StlType::LibCxx> { |
| public: |
| LibCxxForwardListFrontEnd(ValueObject &valobj); |
| |
| llvm::Expected<uint32_t> CalculateNumChildren() override; |
| ValueObjectSP GetChildAtIndex(uint32_t idx) override; |
| lldb::ChildCacheState Update() override; |
| }; |
| |
| class LibCxxListFrontEnd : public AbstractListFrontEnd<StlType::LibCxx> { |
| public: |
| LibCxxListFrontEnd(lldb::ValueObjectSP valobj_sp); |
| |
| llvm::Expected<uint32_t> CalculateNumChildren() override; |
| |
| lldb::ValueObjectSP GetChildAtIndex(uint32_t idx) override; |
| |
| lldb::ChildCacheState Update() override; |
| |
| private: |
| lldb::addr_t m_node_address = 0; |
| ValueObject *m_tail = nullptr; |
| }; |
| |
| class MsvcStlForwardListFrontEnd |
| : public AbstractListFrontEnd<StlType::MsvcStl> { |
| public: |
| MsvcStlForwardListFrontEnd(ValueObject &valobj); |
| |
| llvm::Expected<uint32_t> CalculateNumChildren() override; |
| ValueObjectSP GetChildAtIndex(uint32_t idx) override; |
| lldb::ChildCacheState Update() override; |
| }; |
| |
| class MsvcStlListFrontEnd : public AbstractListFrontEnd<StlType::MsvcStl> { |
| public: |
| MsvcStlListFrontEnd(lldb::ValueObjectSP valobj_sp); |
| |
| llvm::Expected<uint32_t> CalculateNumChildren() override; |
| |
| lldb::ValueObjectSP GetChildAtIndex(uint32_t idx) override; |
| |
| lldb::ChildCacheState Update() override; |
| |
| private: |
| ValueObject *m_tail = nullptr; |
| }; |
| |
| /// Gets the (forward-)list element type from the head node instead of the |
| /// template arguments. This is needed with PDB as it doesn't have info about |
| /// the template arguments. |
| CompilerType GetMsvcStlElementTypeFromHead(ValueObject &head) { |
| auto val_sp = head.GetChildMemberWithName("_Myval"); |
| if (val_sp) |
| return val_sp->GetCompilerType(); |
| return CompilerType(); |
| } |
| |
| } // end anonymous namespace |
| |
| template <StlType Stl> |
| lldb::ChildCacheState AbstractListFrontEnd<Stl>::Update() { |
| m_loop_detected = 0; |
| m_count = UINT32_MAX; |
| m_head = nullptr; |
| m_list_capping_size = 0; |
| m_slow_runner.SetEntry(nullptr); |
| m_fast_runner.SetEntry(nullptr); |
| m_iterators.clear(); |
| |
| if (m_backend.GetTargetSP()) |
| m_list_capping_size = |
| m_backend.GetTargetSP()->GetMaximumNumberOfChildrenToDisplay(); |
| if (m_list_capping_size == 0) |
| m_list_capping_size = 255; |
| |
| CompilerType list_type = m_backend.GetCompilerType(); |
| if (list_type.IsReferenceType()) |
| list_type = list_type.GetNonReferenceType(); |
| |
| if (list_type.GetNumTemplateArguments() == 0) |
| return lldb::ChildCacheState::eRefetch; |
| m_element_type = list_type.GetTypeTemplateArgument(0); |
| |
| return lldb::ChildCacheState::eRefetch; |
| } |
| |
| template <StlType Stl> bool AbstractListFrontEnd<Stl>::HasLoop(size_t count) { |
| if (!g_use_loop_detect) |
| return false; |
| // don't bother checking for a loop if we won't actually need to jump nodes |
| if (m_count < 2) |
| return false; |
| |
| if (m_loop_detected == 0) { |
| // This is the first time we are being run (after the last update). Set up |
| // the loop invariant for the first element. |
| m_slow_runner = ListEntry<Stl>(m_head).next(); |
| m_fast_runner = m_slow_runner.next(); |
| m_loop_detected = 1; |
| } |
| |
| // Loop invariant: |
| // Loop detection has been run over the first m_loop_detected elements. If |
| // m_slow_runner == m_fast_runner then the loop has been detected after |
| // m_loop_detected elements. |
| const size_t steps_to_run = std::min(count, m_count); |
| while (m_loop_detected < steps_to_run && m_slow_runner && m_fast_runner && |
| m_slow_runner != m_fast_runner) { |
| |
| m_slow_runner = m_slow_runner.next(); |
| m_fast_runner = m_fast_runner.next().next(); |
| m_loop_detected++; |
| } |
| if (count <= m_loop_detected) |
| return false; // No loop in the first m_loop_detected elements. |
| if (!m_slow_runner || !m_fast_runner) |
| return false; // Reached the end of the list. Definitely no loops. |
| return m_slow_runner == m_fast_runner; |
| } |
| |
| template <StlType Stl> |
| ValueObjectSP AbstractListFrontEnd<Stl>::GetItem(size_t idx) { |
| size_t advance = idx; |
| ListIterator<Stl> current(m_head); |
| if (idx > 0) { |
| auto cached_iterator = m_iterators.find(idx - 1); |
| if (cached_iterator != m_iterators.end()) { |
| current = cached_iterator->second; |
| advance = 1; |
| } |
| } |
| ValueObjectSP value_sp = current.advance(advance); |
| m_iterators[idx] = current; |
| return value_sp; |
| } |
| |
| LibCxxForwardListFrontEnd::LibCxxForwardListFrontEnd(ValueObject &valobj) |
| : AbstractListFrontEnd(valobj) { |
| Update(); |
| } |
| |
| llvm::Expected<uint32_t> LibCxxForwardListFrontEnd::CalculateNumChildren() { |
| if (m_count != UINT32_MAX) |
| return m_count; |
| |
| ListEntry<StlType::LibCxx> current(m_head); |
| m_count = 0; |
| while (current && m_count < m_list_capping_size) { |
| ++m_count; |
| current = current.next(); |
| } |
| return m_count; |
| } |
| |
| ValueObjectSP LibCxxForwardListFrontEnd::GetChildAtIndex(uint32_t idx) { |
| if (idx >= CalculateNumChildrenIgnoringErrors()) |
| return nullptr; |
| |
| if (!m_head) |
| return nullptr; |
| |
| if (HasLoop(idx + 1)) |
| return nullptr; |
| |
| ValueObjectSP current_sp = GetItem(idx); |
| if (!current_sp) |
| return nullptr; |
| |
| current_sp = current_sp->GetChildAtIndex(1); // get the __value_ child |
| if (!current_sp) |
| return nullptr; |
| |
| // we need to copy current_sp into a new object otherwise we will end up with |
| // all items named __value_ |
| DataExtractor data; |
| Status error; |
| current_sp->GetData(data, error); |
| if (error.Fail()) |
| return nullptr; |
| |
| return CreateValueObjectFromData(llvm::formatv("[{0}]", idx).str(), data, |
| m_backend.GetExecutionContextRef(), |
| m_element_type); |
| } |
| |
| lldb::ChildCacheState LibCxxForwardListFrontEnd::Update() { |
| AbstractListFrontEnd::Update(); |
| |
| Status err; |
| ValueObjectSP backend_addr(m_backend.AddressOf(err)); |
| if (err.Fail() || !backend_addr) |
| return lldb::ChildCacheState::eRefetch; |
| |
| auto list_base_sp = m_backend.GetChildAtIndex(0); |
| if (!list_base_sp) |
| return lldb::ChildCacheState::eRefetch; |
| |
| // Anonymous strucutre index is in base class at index 0. |
| auto [impl_sp, is_compressed_pair] = GetValueOrOldCompressedPair( |
| *list_base_sp, "__before_begin_", "__before_begin_"); |
| if (!impl_sp) |
| return ChildCacheState::eRefetch; |
| |
| if (is_compressed_pair) |
| impl_sp = GetFirstValueOfLibCXXCompressedPair(*impl_sp); |
| |
| if (!impl_sp) |
| return ChildCacheState::eRefetch; |
| |
| m_head = impl_sp->GetChildMemberWithName("__next_").get(); |
| |
| return ChildCacheState::eRefetch; |
| } |
| |
| LibCxxListFrontEnd::LibCxxListFrontEnd(lldb::ValueObjectSP valobj_sp) |
| : AbstractListFrontEnd(*valobj_sp) { |
| if (valobj_sp) |
| Update(); |
| } |
| |
| llvm::Expected<uint32_t> LibCxxListFrontEnd::CalculateNumChildren() { |
| if (m_count != UINT32_MAX) |
| return m_count; |
| if (!m_head || !m_tail || m_node_address == 0) |
| return 0; |
| |
| auto [size_node_sp, is_compressed_pair] = |
| GetValueOrOldCompressedPair(m_backend, "__size_", "__size_alloc_"); |
| if (is_compressed_pair) |
| size_node_sp = GetFirstValueOfLibCXXCompressedPair(*size_node_sp); |
| |
| if (size_node_sp) |
| m_count = size_node_sp->GetValueAsUnsigned(UINT32_MAX); |
| |
| if (m_count != UINT32_MAX) |
| return m_count; |
| |
| uint64_t next_val = m_head->GetValueAsUnsigned(0); |
| uint64_t prev_val = m_tail->GetValueAsUnsigned(0); |
| if (next_val == 0 || prev_val == 0) |
| return 0; |
| if (next_val == m_node_address) |
| return 0; |
| if (next_val == prev_val) |
| return 1; |
| uint64_t size = 2; |
| ListEntry<StlType::LibCxx> current(m_head); |
| while (current.next() && current.next().value() != m_node_address) { |
| size++; |
| current = current.next(); |
| if (size > m_list_capping_size) |
| break; |
| } |
| return m_count = (size - 1); |
| } |
| |
| lldb::ValueObjectSP LibCxxListFrontEnd::GetChildAtIndex(uint32_t idx) { |
| static ConstString g_value("__value_"); |
| static ConstString g_next("__next_"); |
| |
| if (idx >= CalculateNumChildrenIgnoringErrors()) |
| return lldb::ValueObjectSP(); |
| |
| if (!m_head || !m_tail || m_node_address == 0) |
| return lldb::ValueObjectSP(); |
| |
| if (HasLoop(idx + 1)) |
| return lldb::ValueObjectSP(); |
| |
| ValueObjectSP current_sp = GetItem(idx); |
| if (!current_sp) |
| return lldb::ValueObjectSP(); |
| |
| current_sp = current_sp->GetChildAtIndex(1); // get the __value_ child |
| if (!current_sp) |
| return lldb::ValueObjectSP(); |
| |
| if (current_sp->GetName() == g_next) { |
| ProcessSP process_sp(current_sp->GetProcessSP()); |
| if (!process_sp) |
| return lldb::ValueObjectSP(); |
| |
| // if we grabbed the __next_ pointer, then the child is one pointer deep-er |
| lldb::addr_t addr = current_sp->GetParent()->GetPointerValue().address; |
| addr = addr + 2 * process_sp->GetAddressByteSize(); |
| ExecutionContext exe_ctx(process_sp); |
| current_sp = |
| CreateValueObjectFromAddress("__value_", addr, exe_ctx, m_element_type); |
| if (!current_sp) |
| return lldb::ValueObjectSP(); |
| } |
| |
| // we need to copy current_sp into a new object otherwise we will end up with |
| // all items named __value_ |
| DataExtractor data; |
| Status error; |
| current_sp->GetData(data, error); |
| if (error.Fail()) |
| return lldb::ValueObjectSP(); |
| |
| StreamString name; |
| name.Printf("[%" PRIu64 "]", (uint64_t)idx); |
| return CreateValueObjectFromData(name.GetString(), data, |
| m_backend.GetExecutionContextRef(), |
| m_element_type); |
| } |
| |
| lldb::ChildCacheState LibCxxListFrontEnd::Update() { |
| AbstractListFrontEnd::Update(); |
| m_tail = nullptr; |
| m_node_address = 0; |
| |
| Status err; |
| ValueObjectSP backend_addr(m_backend.AddressOf(err)); |
| if (err.Fail() || !backend_addr) |
| return lldb::ChildCacheState::eRefetch; |
| m_node_address = backend_addr->GetValueAsUnsigned(0); |
| if (!m_node_address || m_node_address == LLDB_INVALID_ADDRESS) |
| return lldb::ChildCacheState::eRefetch; |
| ValueObjectSP impl_sp(m_backend.GetChildMemberWithName("__end_")); |
| if (!impl_sp) |
| return lldb::ChildCacheState::eRefetch; |
| m_head = impl_sp->GetChildMemberWithName("__next_").get(); |
| m_tail = impl_sp->GetChildMemberWithName("__prev_").get(); |
| return lldb::ChildCacheState::eRefetch; |
| } |
| |
| MsvcStlForwardListFrontEnd::MsvcStlForwardListFrontEnd(ValueObject &valobj) |
| : AbstractListFrontEnd(valobj) { |
| Update(); |
| } |
| |
| llvm::Expected<uint32_t> MsvcStlForwardListFrontEnd::CalculateNumChildren() { |
| if (m_count != UINT32_MAX) |
| return m_count; |
| |
| ListEntry<StlType::MsvcStl> current(m_head); |
| m_count = 0; |
| while (current && m_count < m_list_capping_size) { |
| ++m_count; |
| current = current.next(); |
| } |
| return m_count; |
| } |
| |
| ValueObjectSP MsvcStlForwardListFrontEnd::GetChildAtIndex(uint32_t idx) { |
| if (idx >= CalculateNumChildrenIgnoringErrors()) |
| return nullptr; |
| |
| if (!m_head) |
| return nullptr; |
| |
| if (HasLoop(idx + 1)) |
| return nullptr; |
| |
| ValueObjectSP current_sp = GetItem(idx); |
| if (!current_sp) |
| return nullptr; |
| |
| current_sp = current_sp->GetChildAtIndex(1); // get the _Myval child |
| if (!current_sp) |
| return nullptr; |
| |
| // we need to copy current_sp into a new object otherwise we will end up with |
| // all items named _Myval |
| DataExtractor data; |
| Status error; |
| current_sp->GetData(data, error); |
| if (error.Fail()) |
| return nullptr; |
| |
| return CreateValueObjectFromData(llvm::formatv("[{0}]", idx).str(), data, |
| m_backend.GetExecutionContextRef(), |
| m_element_type); |
| } |
| |
| lldb::ChildCacheState MsvcStlForwardListFrontEnd::Update() { |
| AbstractListFrontEnd::Update(); |
| |
| if (auto head_sp = |
| m_backend.GetChildAtNamePath({"_Mypair", "_Myval2", "_Myhead"})) |
| m_head = head_sp.get(); |
| |
| // With PDB, we can't get the element type from the template arguments |
| if (!m_element_type && m_head) |
| m_element_type = GetMsvcStlElementTypeFromHead(*m_head); |
| |
| return ChildCacheState::eRefetch; |
| } |
| |
| MsvcStlListFrontEnd::MsvcStlListFrontEnd(lldb::ValueObjectSP valobj_sp) |
| : AbstractListFrontEnd(*valobj_sp) { |
| if (valobj_sp) |
| Update(); |
| } |
| |
| llvm::Expected<uint32_t> MsvcStlListFrontEnd::CalculateNumChildren() { |
| if (m_count != UINT32_MAX) |
| return m_count; |
| if (!m_head || !m_tail) |
| return 0; |
| |
| auto size_sp = |
| m_backend.GetChildAtNamePath({"_Mypair", "_Myval2", "_Mysize"}); |
| if (!size_sp) |
| return llvm::createStringError("Failed to resolve size."); |
| |
| m_count = size_sp->GetValueAsUnsigned(UINT32_MAX); |
| if (m_count == UINT32_MAX) |
| return llvm::createStringError("Failed to read size value."); |
| |
| return m_count; |
| } |
| |
| lldb::ValueObjectSP MsvcStlListFrontEnd::GetChildAtIndex(uint32_t idx) { |
| if (idx >= CalculateNumChildrenIgnoringErrors()) |
| return lldb::ValueObjectSP(); |
| |
| if (!m_head || !m_tail) |
| return lldb::ValueObjectSP(); |
| |
| if (HasLoop(idx + 1)) |
| return lldb::ValueObjectSP(); |
| |
| ValueObjectSP current_sp = GetItem(idx); |
| if (!current_sp) |
| return lldb::ValueObjectSP(); |
| |
| current_sp = current_sp->GetChildAtIndex(2); // get the _Myval child |
| if (!current_sp) |
| return lldb::ValueObjectSP(); |
| |
| // we need to copy current_sp into a new object otherwise we will end up with |
| // all items named _Myval |
| DataExtractor data; |
| Status error; |
| current_sp->GetData(data, error); |
| if (error.Fail()) |
| return lldb::ValueObjectSP(); |
| |
| StreamString name; |
| name.Printf("[%" PRIu64 "]", (uint64_t)idx); |
| return CreateValueObjectFromData(name.GetString(), data, |
| m_backend.GetExecutionContextRef(), |
| m_element_type); |
| } |
| |
| lldb::ChildCacheState MsvcStlListFrontEnd::Update() { |
| AbstractListFrontEnd::Update(); |
| m_tail = nullptr; |
| m_head = nullptr; |
| |
| ValueObjectSP last = |
| m_backend.GetChildAtNamePath({"_Mypair", "_Myval2", "_Myhead"}); |
| if (!last) |
| return lldb::ChildCacheState::eRefetch; |
| ValueObjectSP first = last->GetChildMemberWithName("_Next"); |
| if (!first) |
| return lldb::ChildCacheState::eRefetch; |
| |
| m_head = first.get(); |
| m_tail = last.get(); |
| |
| // With PDB, we can't get the element type from the template arguments |
| if (!m_element_type && m_head) |
| m_element_type = GetMsvcStlElementTypeFromHead(*m_head); |
| |
| return lldb::ChildCacheState::eRefetch; |
| } |
| |
| SyntheticChildrenFrontEnd *formatters::LibcxxStdListSyntheticFrontEndCreator( |
| CXXSyntheticChildren *, lldb::ValueObjectSP valobj_sp) { |
| return (valobj_sp ? new LibCxxListFrontEnd(valobj_sp) : nullptr); |
| } |
| |
| SyntheticChildrenFrontEnd * |
| formatters::LibcxxStdForwardListSyntheticFrontEndCreator( |
| CXXSyntheticChildren *, lldb::ValueObjectSP valobj_sp) { |
| return valobj_sp ? new LibCxxForwardListFrontEnd(*valobj_sp) : nullptr; |
| } |
| |
| bool formatters::IsMsvcStlList(ValueObject &valobj) { |
| if (auto valobj_sp = valobj.GetNonSyntheticValue()) |
| return valobj_sp->GetChildMemberWithName("_Mypair") != nullptr; |
| |
| return false; |
| } |
| |
| SyntheticChildrenFrontEnd * |
| formatters::MsvcStlListSyntheticFrontEndCreator(CXXSyntheticChildren *, |
| lldb::ValueObjectSP valobj_sp) { |
| return (valobj_sp ? new MsvcStlListFrontEnd(valobj_sp) : nullptr); |
| } |
| |
| SyntheticChildrenFrontEnd * |
| formatters::MsvcStlForwardListSyntheticFrontEndCreator( |
| CXXSyntheticChildren *, lldb::ValueObjectSP valobj_sp) { |
| return valobj_sp ? new MsvcStlForwardListFrontEnd(*valobj_sp) : nullptr; |
| } |