| //===-- 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: | |
| 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 CreateChildValueObjectFromData(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 = CreateChildValueObjectFromAddress("__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 CreateChildValueObjectFromData(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 CreateChildValueObjectFromData(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 CreateChildValueObjectFromData(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; | |
| } |