Visual Studio 2010和2008无法处理不同文件夹中具有相同名称的源文件?

68

直接问题:如果我有两个文件同名(但在不同的目录中),似乎只有 Visual Studio 2005 可以透明地处理?VS 2008 和 2010 需要进行大量调整吗?除了我的命名约定之外,我是否做错了什么?

背景:

我正在开发 C++ 统计库... 我有两个文件夹:

/ Univariate

Normal.cpp
Normal.h
Beta.cpp
Beta.h
Adaptive.cpp
Adaptive.h

/ Multivariate

Normal.cpp
Normal.h
Beta.cpp
Beta.h
Adaptive.cpp
Adaptive.h

我需要支持交叉编译——使用g++/make在Linux中将这些文件编译成库,它们可以正常工作。

之前我一直在使用Visual Studio 2005,并没有问题,但是我需要升级到Visual Studio 2008或2010(目前非常喜欢nVidia的nsight工具)。然而,如果我将同名文件添加到项目中(即使它们在不同的目录下),我就遇到了问题。我愿意改变我的命名约定,但我很好奇其他人是否遇到过这个问题并找到了一些充分文档化的解决方案?

更让我困惑的是,如果我从2005项目升级到2010项目,似乎VS 2010能够正确处理位于不同目录中同名的两个源文件;但是,如果我将其中一个重复文件删除,然后将其重新添加到项目中,就会收到以下警告:

Distributions\Release\Adaptive.obj : warning LNK4042: object specified more than once; extras ignored

现在我将中间目录指定为$(ProjectName)\$(Configuration),我需要将我的目标文件放在与源树不同的位置。所以我可以理解为什么它会将目标文件复制到彼此上面,但是当项目从2005转换到2008或2010时,会添加一堆条件编译:

<ObjectFileName Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)%(Filename)1.obj</ObjectFileName>
<XMLDocumentationFileName Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)%(Filename)1.xdc</XMLDocumentationFileName>

您可以在C/C++的源文件属性页面中找到它们,选择“输出文件”->“对象文件名”和“XML文档文件名”。但是,如果我直接添加文件(或删除并重新添加它们),VS不会在编译之前发出警告,但也永远不会添加条件指令——因此,为了正确工作,我必须为每个单独的配置手动添加条件指令。我是犯了错误/错误的假设还是发现了VS 2008/2010中的有效错误?


2
可能是Visual Studio 2010的奇怪“warning LNK4042”的重复问题。 - cHao
哇 - 太棒了,cHao!所以,只需要咬咬牙,要么更改名称,要么默认编辑属性页面。如果我找到任何其他有用的文件特定变量,我将在下面添加更多注释。 - M. Tibbits
@M. Tibbits:对于重复的问题,我很抱歉,我应该使用更具描述性的标题:/ 我个人选择了不同的名称“solution”。 - Matthieu M.
@M Tibbits:我肯定不会感到不便,我得到的答案都不令人满意(尽管我非常怀疑这是由于VS而不是这个社区:p)。 - Matthieu M.
翻译文本:痛苦无比。 :( - tofutim
显示剩余2条评论
8个回答

80

所以,@Hans Passant给了正确的指引,谢谢!您不需要列出文件,只需提供文件夹即可。然后,如果您查看VS 2010列表底部的定义的宏,您将会看到:

%(RelativeDir)/ Univariate/

如发布的问题实际上是我正在处理的一个简化版本——在单个项目中有几个层级的文件夹,并且有几个名称冲突。因此,我真正想要的是某种方式来“修复”它...

如果您在解决方案资源管理器中右键单击该项目,选择C/C++ -> “输出文件”,然后在“对象文件名”框中输入以下内容:

$(IntDir)/%(RelativeDir)/

请注意,我还从下拉菜单中选择了(所有配置,所有平台)。这将编译源代码树镜像的目录层次结构中的每个文件。 VS2010将通过创建这些目录来开始构建,如果它们不存在。另外,对于那些厌恶其目录名称中的空格的人来说,此宏确实会删除所有空格,因此在使用时无需玩弄双引号。

这正是我想要的——与Ubuntu侧的Makefiles工作方式完全相同,同时仍使源代码树保持整洁。


1
我无法在VS 2013中使这些技术之一起作用。 - evoskuil
3
我的项目比源代码深几个层级,所以相对目录的值为../../src/之类的,但我发现使用$(IntDir)/fake/dir/%(RelativeDir)/可以抵消两个点号的影响。 - TheJosh
这在Visual Studio 2012更新1中有效,但自从我升级到更新4后,VS似乎不再想为目标文件创建中间目录了。 :((请参见http://stackoverflow.com/questions/30212698。) - Thomas Young
2
起初我无法在VS宏中找到%(RelativeDir),但是这篇文章帮了我。尽管我的对象最终出现在解决方案的顶级根构建中。所以我使用了以下代码:$(IntDir)\$(ProjectName)\$(ConfigurationName)\%(RelativeDir) - Rado
很奇怪,在2022年它仍然不是默认值,我认为在不同文件夹中有两个具有相同名称的源文件并不是什么不专业或罕见的事情。 - mhn2
显示剩余4条评论

14

在IDE中很容易解决。点击文件夹中的第一个文件,Shift+点击最后一个文件以选择所有文件。右键单击,选择属性,C ++,输出文件。将Object File Name从$(IntDir)\更改为,例如,$(IntDir)\Univariate\。您可以重复进行多元文件组,尽管这不是必须的。


10

你说得对,VS无法处理这个问题,以前也一直如此。根本问题在于它为项目中的每个.cpp文件生成一个.obj文件,并将它们全部放置在同一个文件夹中。因此,例如在你的情况下,你最终会得到多个.cpp文件编译成Adaptive.obj。

至少链接器现在会为此生成警告了。这并不总是这样。

你应该能够通过确保文件使用不同的Intermediate Directory路径来解决这个问题,但这是一种解决方法,而不是真正解决问题的方法。

当然,你可以在Microsoft Connect上提交错误报告或功能请求。


2
我认为这个问题可能已经存在了 此链接。那里推荐的其中一个解决方法是手动更改一个(或两个)目标文件的生成名称。你应该能够覆盖所有配置,因此实现起来不应该太繁琐。 - Mike Ellery
谢谢您的建议,我已经将其提交给Microsoft Connect。正如Mike Ellery所指出的那样,这之前是一个bug,但技术人员声称在2010年就已经修复了——我可以确认它没有被修复。如果您或其他人有能力,可以自由地链接这两个报告:<a href="https://connect.microsoft.com/VisualStudio/feedback/details/599534/unable-to-compile-by-default-files-with-the-same-name-in-different-directories">VS 2010 Bug Report</a>。 - M. Tibbits

9
请注意,在我的实例中(使用VS2010平台工具集的Visual Studio 2013),使用$(IntDir)\%(RelativeDir)不起作用,它忽略了中间目录,这会在构建多个配置时导致链接器错误,因为每个配置(即Debug和Release)的所有对象文件都放置在同一个文件夹中。如果在切换配置时清理项目,这些错误将消失。 错误示例:

MSVCRTD.lib(MSVCR100D.dll) : error LNK2005: _fclose already defined in LIBCMTD.lib(fclose.obj)

解决方案:

$(IntDir)\%(Directory)

为了解决这个问题,我必须使用$(IntDir)\%(Directory),这样可以正确地将所有*.obj文件放置在中间目录下,并允许构建和链接多个配置而无需清理。唯一的缺点是您的文件所在的整个(可能很长)文件夹层次结构将完全重新创建在Debug/Release等文件夹下。

1
感谢 %(Directory)。它与 %(RelativeDir) 不同,在我的情况下很重要。 - Ilya Matveychikov
这个解决方案非常棒,它可以使用项目中上层文件夹的路径,例如 <ClCompile Include="..\..\..\sources\folder\**\*.cpp" /> - Vbif
1
这应该是默认设置!我认为 $(IntDir)%(Directory) 足够了(不需要 \)。 - tower120

8
这个帖子中的其他解决方案存在基于RelativeDir的../..问题,并且需要在每个源文件上手动设置。更不用说,它们会破坏/MP。任何指定确切.obj文件的解决方案都将导致每个.cpp文件(将其映射到特定的.obj文件)的不同/Fo传递给CL.exe,因此Visual Studio无法对它们进行批处理。如果没有批处理几个具有相同命令行(包括/Fo)的.cpp文件,则/MP无法工作。以下是一种新方法。这适用于至少vs2010到vs2015。将其添加到中的vcxproj中。
<!-- ================ UNDUPOBJ ================ -->
<!-- relevant topics -->
<!-- https://dev59.com/tW865IYBdhLWcg3wnP2a#26935613 -->
<!-- https://dev59.com/R1rUa4cB1Zd3GeqPhS-U -->
<!-- https://dev59.com/-HXYa4cB1Zd3GeqP-tN4 -->
<!-- other maybe related info -->
<!-- https://dev59.com/5nRA5IYBdhLWcg3wvgob -->
<UsingTask TaskName="UNDUPOBJ_TASK" TaskFactory="CodeTaskFactory" AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.v4.0.dll">
  <ParameterGroup>
    <OutputDir ParameterType="System.String" Required="true" />
    <ItemList ParameterType="Microsoft.Build.Framework.ITaskItem[]" Required="true" />
    <OutputItemList ParameterType="Microsoft.Build.Framework.ITaskItem[]" Output="true" />
  </ParameterGroup>
  <Task>
    <Code><![CDATA[
            //general outline: for each item (in ClCompile) assign it to a subdirectory of $(IntDir) by allocating subdirectories 0,1,2, etc., as needed to prevent duplicate filenames from clobbering each other
            //this minimizes the number of batches that need to be run, since each subdirectory will necessarily be in a distinct batch due to /Fo specifying that output subdirectory

            var assignmentMap = new Dictionary<string,int>();
            HashSet<string> neededDirectories = new HashSet<string>();
            foreach( var item in ItemList )
            {
              //solve bug e.g. Checkbox.cpp vs CheckBox.cpp
              var filename = item.GetMetadata("Filename").ToUpperInvariant(); 

              //assign reused filenames to increasing numbers
              //assign previously unused filenames to 0
              int assignment = 0;
              if(assignmentMap.TryGetValue(filename, out assignment))
                assignmentMap[filename] = ++assignment;
              else
                assignmentMap[filename] = 0;

              var thisFileOutdir = Path.Combine(OutputDir,assignment.ToString()) + "/"; //take care it ends in / so /Fo knows it's a directory and not a filename
              item.SetMetadata( "ObjectFileName", thisFileOutdir );
            }

            foreach(var needed in neededDirectories)
              System.IO.Directory.CreateDirectory(needed);

            OutputItemList = ItemList;
            ItemList = new Microsoft.Build.Framework.ITaskItem[0];

        ]]></Code>
  </Task>
</UsingTask>

<Target Name="UNDUPOBJ">
  <!-- see stackoverflow topics for discussion on why we need to do some loopy copying stuff here -->
  <ItemGroup>
    <ClCompileCopy Include="@(ClCompile)"/>
    <ClCompile Remove="@(ClCompile)"/>
  </ItemGroup>
  <UNDUPOBJ_TASK OutputDir="$(IntDir)" ItemList="@(ClCompileCopy)" OutputItemList="@(ClCompile)">
    <Output ItemName="ClCompile" TaskParameter="OutputItemList"/>
  </UNDUPOBJ_TASK>
</Target>
<!-- ================ UNDUPOBJ ================ -->

然后修改<project>,使其读取:

<Project InitialTargets="UNDUPOBJ" ...

结果将类似于 myproj/src/a/x.cpp 和 myproj/src/b/x.cpp 编译为 Debug/0/x.obj 和 Debug/1/x.obj。RelativeDirs 没有被使用,因此不是问题。
此外,在这种情况下,只会传递两个不同的 /Fo 给 CL.exe:Debug/0/ 和 Debug/1/。因此,最多只会向 CL.exe 发出两个批处理,从而使 /MP 更有效地工作。
其他方法可能基于 .cpp 子目录构建 .obj 子目录,或者使 .obj 文件名包含一些原始 .cpp 目录的提示,以便您可以轻松查看 .cpp->.obj 映射,但这些方法会导致更多的 /Fo,因此批处理较少。未来的工作可以转储映射文件以进行快速参考。
有关 /MP 和批处理的更多详细信息,请参见:MSVC10 /MP builds not multicore across folders in a project 我已在各种工具链上的 vs2010 和 vs2015 中进行了生产测试。它似乎非常可靠,但总有可能与其他 msbuild 自定义或异国工具链产生不良交互。

从vs2015开始,如果您收到警告“warning MSB8027: Two or more files with the name of X.cpp will produce outputs to the same location”,则可以将以下内容添加到您的项目或msbuild文件中:

<PropertyGroup Label="Globals"><IgnoreWarnCompileDuplicatedFilename>true</IgnoreWarnCompileDuplicatedFilename></PropertyGroup>

查看更多内容请访问https://connect.microsoft.com/VisualStudio/feedback/details/797460/incorrect-warning-msb8027-reported-for-files-excluded-from-build如何抑制特定的MSBuild警告


2019年9月更新:

我认为以上内容存在问题,其中ItemList可以任意排序,导致给定源文件重新分配到不同编号的目录中。因此,每当源文件列表发生变化时,对象的副本可能会出现重复。这也可能导致依赖跟踪有些混乱,因为输入输出映射可能会突然改变,使时间戳毫无意义。

我们可以通过对排序后的ItemList进行哈希并将其作为前缀添加到输出目录中来解决此问题,但这将创建额外的重建和大量的垃圾。

这可以通过放弃列出输入输出映射的文件,并使源文件到目标编号目录的映射成为一次性事件(直到项目被清理)来进一步完善。

我还需要深入调查此问题。


6

使用配置属性 > C/C++ > 输出文件 > $(IntDir)\%(RelativeDir)\%(Filename)

这将在调试目录下复制源文件结构,并将每个目录的对象文件放置在同名文件夹下的 Debug 目录中。


已确认VS2013。我将 $(IntDir)%(RelativeDir) 添加到“配置属性 > C/C++ > 输出文件 > 对象文件名”中。 - Tom

1

还有一种情况是你在Visual Studio中创建了一个.cpp文件,然后将它重命名为.h。虽然文件被重命名了,但Visual Studio仍会将其编译为cpp文件,因此会创建两个obj文件并显示链接器警告。


0

%(RelativeDir)解决方案仅适用于Visual Studio 2010。

对于Visual Studio 2008,您需要右键单击每个重复的.cpp文件,并选择“属性”。可能不明显,但实际上您可以修改每个单独文件的“配置属性-> C / C ++->输出文件”部分。

为每个重复的.cpp文件的“对象文件名”设置添加一个子文件夹,注意不需要修改.h文件,因为.cpp文件确定了.obj文件输出。

例如,如果您的项目有两个冲突的文件:

internal\example.cpp base\example.cpp

你需要为每个设置"对象文件名": $(IntDir)\internal\ $(IntDir)\base\
你需要对所有配置(Release/Debug等)都这样做。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接